Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
13 kB · 354 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355import { Container, setKeybindings } from "@earendil-works/pi-tui";import stripAnsi from "strip-ansi";import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";import { KeybindingsManager } from "../src/core/keybindings.js";import { BrandSplashHeader, getRandomStartHint, InteractiveMode, START_HINTS,} from "../src/modes/interactive/interactive-mode.js";import type { PromptStashState } from "../src/modes/interactive/prompt-stash-state.js";import { getMarkdownTheme, initTheme } from "../src/modes/interactive/theme/theme.js";
describe("InteractiveMode startup hints", () => { beforeAll(() => { initTheme("dark"); });
beforeEach(() => { setKeybindings(new KeybindingsManager()); });
function createMode(messageCount = 0, returnToAgentsView = false, getEditorText = () => "") { const mode = { options: { returnToAgentsView }, editor: { getText: getEditorText }, connectionState: { model: { name: "test-model", reasoning: true }, thinkingLevel: "high", messageCount, isStreaming: false, remoteHost: undefined as string | undefined, }, }; Object.setPrototypeOf(mode, InteractiveMode.prototype); return mode; }
it("keeps a blank row above the shared splash and limits its metadata", () => { const header = new BrandSplashHeader( "0.0.0", () => "test-model", () => "/tmp/project", undefined, { topPadding: true, getStartHint: () => 'Try "refactor @<filepath>"', }, );
const lines = header.render(120); const output = stripAnsi(lines.join("\n"));
expect(lines[0]).toBe(""); expect(output).toContain("version v0.0.0"); expect(output).toContain("model test-model"); expect(output).toContain("cwd /tmp/project"); expect(output).toContain('Try "refactor @<filepath>"'); expect(output).not.toContain("input"); expect(output).not.toContain("files"); expect(output).not.toContain("help");
const unpadded = new BrandSplashHeader( "0.0.0", () => "test-model", () => "/tmp/project", ); expect(unpadded.render(120)[0]).not.toBe(""); });
it("randomly selects from five concise filepath prompts", () => { expect(START_HINTS).toHaveLength(5); expect(new Set(START_HINTS).size).toBe(5);
for (const [index, hint] of START_HINTS.entries()) { expect(getRandomStartHint(() => index / START_HINTS.length)).toBe(hint); expect(hint).toMatch(/^Try ".*@<filepath>.*"$/); } });
it("places the fresh-chat shortcut hint after the model and effort", () => { const mode = createMode(); const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode);
expect(stripAnsi(label)).toBe("test-model • high ? for shortcuts"); });
it("displays remote host after model in tray location label", () => { const mode = createMode(); mode.connectionState.remoteHost = "chernobog"; const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode);
expect(stripAnsi(label)).toBe("test-model • high on chernobog ? for shortcuts"); expect(label).toContain("on"); expect(label).toContain("chernobog"); });
it("renders remote host in splash header metadata when provided", () => { const header = new BrandSplashHeader( "0.0.0", () => "test-model", () => "/tmp/project", undefined, { topPadding: true, getExtraMetadata: () => [{ label: "host", value: "chernobog" }], }, );
const lines = header.render(120); const output = stripAnsi(lines.join("\n"));
expect(output).toContain("host chernobog"); });
it("keeps fresh-chat guidance hidden when a mid-turn snapshot still has no committed messages", () => { const mode = createMode(); const patchConnectionState = (patch: Record<string, unknown>) => Object.assign(mode.connectionState, patch); Object.assign(mode, { patchConnectionState, builtInHeader: { invalidate: vi.fn() }, subagentSummaryLine: { invalidate: vi.fn() }, }); const updateConnectionStateFromEvent = Reflect.get( InteractiveMode.prototype, "updateConnectionStateFromEvent", ) as (event: unknown) => void; const getLabel = () => stripAnsi(Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode)); const message = { role: "user", content: "hello", timestamp: 1 };
updateConnectionStateFromEvent.call(mode, { type: "agent_start" }); updateConnectionStateFromEvent.call(mode, { type: "message_start", message }); Object.assign(mode.connectionState, { messageCount: 0, isStreaming: true });
expect(getLabel()).not.toContain("for shortcuts");
updateConnectionStateFromEvent.call(mode, { type: "message_end", message }); updateConnectionStateFromEvent.call(mode, { type: "agent_end", messages: [message] }); expect(getLabel()).not.toContain("for shortcuts"); });
it("routes session-view requests through the existing agents-view return path", async () => { const returnToAgentsView = vi.fn(async () => {}); const mode = Object.assign(createMode(0, true), { returnToAgentsView });
await Reflect.get(InteractiveMode.prototype, "requestAgentsView").call(mode);
expect(returnToAgentsView).toHaveBeenCalledOnce(); });
it("no longer blocks the agents-view handoff on a draft", async () => { const returnToAgentsView = vi.fn(async () => {}); const showStatus = vi.fn(); const mode = Object.assign( createMode(0, true, () => "draft prompt"), { returnToAgentsView, showStatus }, );
await Reflect.get(InteractiveMode.prototype, "requestAgentsView").call(mode);
expect(returnToAgentsView).toHaveBeenCalledOnce(); expect(showStatus).not.toHaveBeenCalled(); });
it("no longer blocks the scoped agents-view handoff on a draft", async () => { const returnToAgentsView = vi.fn(async () => {}); const showStatus = vi.fn(); const mode = Object.assign( createMode(0, true, () => "scoped draft"), { returnToAgentsView, showStatus }, );
await Reflect.get(InteractiveMode.prototype, "openScopedAgentsView").call(mode);
expect(returnToAgentsView).toHaveBeenCalledWith("scoped_agents_view"); expect(showStatus).not.toHaveBeenCalled(); });
it("stashes the draft once per agents-view handoff even when re-requested mid-teardown", async () => { const promptStashState: PromptStashState = {}; let resolveDispose!: () => void; const disposePromise = new Promise<void>((resolve) => { resolveDispose = resolve; }); const mode = Object.assign( createMode(0, true, () => "draft prompt"), { promptStashState, pastedImages: new Map(), isShuttingDown: false, agentsViewRequest: undefined, unregisterSignalHandlers: vi.fn(), teardownSessionUi: vi.fn(async () => {}), agentConnection: { dispose: vi.fn(() => disposePromise) }, }, ); const returnToAgentsView = Reflect.get(InteractiveMode.prototype, "returnToAgentsView");
const firstHandoff = returnToAgentsView.call(mode); await returnToAgentsView.call(mode);
expect(promptStashState.stash).toMatchObject({ text: "draft prompt", restoreOnOpen: true }); expect(promptStashState.queuedStashes).toBeUndefined(); resolveDispose(); await firstHandoff; });
it("opens the shared session view on back navigation for process-local chats", async () => { const requestAgentsView = vi.fn(async () => {}); const returnToAgentsView = vi.fn(async () => {}); const mode = Object.assign(createMode(0, false), { requestAgentsView, returnToAgentsView });
const handled = Reflect.get(InteractiveMode.prototype, "handleAgentsBack").call(mode) as boolean;
expect(handled).toBe(true); expect(requestAgentsView).toHaveBeenCalledOnce(); expect(returnToAgentsView).not.toHaveBeenCalled(); });
it("returns to the daemon agents view on back navigation for daemon chats", async () => { const requestAgentsView = vi.fn(async () => {}); const returnToAgentsView = vi.fn(async () => {}); const mode = Object.assign(createMode(0, true), { requestAgentsView, returnToAgentsView });
const handled = Reflect.get(InteractiveMode.prototype, "handleAgentsBack").call(mode) as boolean;
expect(handled).toBe(true); expect(returnToAgentsView).toHaveBeenCalledOnce(); expect(requestAgentsView).not.toHaveBeenCalled(); });
it("leaves back navigation to the editor while a draft exists", async () => { const requestAgentsView = vi.fn(async () => {}); const mode = Object.assign( createMode(0, false, () => "draft prompt"), { requestAgentsView }, );
const handled = Reflect.get(InteractiveMode.prototype, "handleAgentsBack").call(mode) as boolean;
expect(handled).toBe(false); expect(requestAgentsView).not.toHaveBeenCalled(); });
it("explains that the agents view needs the daemon for non-daemon chats", async () => { const showStatus = vi.fn(); const shutdown = vi.fn(async () => {}); const mode = Object.assign(createMode(0, false), { returnToAgentsView: vi.fn(async () => {}), showStatus, shutdown, });
await Reflect.get(InteractiveMode.prototype, "requestAgentsView").call(mode);
expect(showStatus).toHaveBeenCalledWith(expect.stringContaining("needs the daemon")); expect(shutdown).not.toHaveBeenCalled(); });
it("keeps the lowercase agents hint while typing", () => { let editorText = ""; const mode = createMode(0, true, () => editorText); const getLabel = () => Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode);
expect(stripAnsi(getLabel())).toBe("← agents/resume test-model • high ? for shortcuts");
editorText = "draft prompt"; expect(stripAnsi(getLabel())).toBe("← agents/resume test-model • high"); });
it("hides the fresh-chat shortcut hint while the prompt has text", () => { let editorText = ""; const mode = createMode(0, false, () => editorText); const getLabel = () => Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode);
expect(stripAnsi(getLabel())).toBe("test-model • high ? for shortcuts");
editorText = "draft prompt"; expect(stripAnsi(getLabel())).toBe("test-model • high");
editorText = " "; expect(stripAnsi(getLabel())).toBe("test-model • high");
editorText = ""; expect(stripAnsi(getLabel())).toBe("test-model • high ? for shortcuts"); });
it("hides the tray shortcut guidance for chats with history", () => { const mode = createMode(1); const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode);
expect(stripAnsi(label)).toBe("test-model • high"); });
it("keeps the question-mark shortcut guide compact", () => { const guide = Reflect.get(InteractiveMode.prototype, "getShortcutGuide").call(createMode());
expect(guide).toContain("`!` shell mode · `/` commands · `@` file paths"); expect(guide).toContain("stash prompt"); expect(guide).toContain("`/hotkeys` full reference"); expect(guide).not.toContain("Ctrl+Z"); expect(guide).not.toContain("suspend"); expect(guide).not.toContain("**Navigation**"); expect(guide).not.toContain("**Extensions**"); });
it("renders question-mark shortcut help ephemerally without appending to chat history", () => { const shortcutGuideContainer = new Container(); const chatContainer = new Container(); const mode = Object.assign(createMode(), { shortcutGuideContainer, chatContainer, ui: { requestRender: vi.fn() }, getMarkdownThemeWithSettings: () => getMarkdownTheme(), });
Reflect.get(InteractiveMode.prototype, "showShortcutGuide").call(mode); Reflect.get(InteractiveMode.prototype, "showShortcutGuide").call(mode);
expect(chatContainer.children).toHaveLength(0); expect(shortcutGuideContainer.children).toHaveLength(2);
Reflect.get(InteractiveMode.prototype, "clearShortcutGuide").call(mode);
expect(shortcutGuideContainer.children).toHaveLength(0); });
it("keeps /hotkeys comprehensive without Ctrl+Z", () => { const guide = Reflect.get(InteractiveMode.prototype, "getHotkeysGuide").call(createMode());
expect(guide).toContain("**Navigation**"); expect(guide).toContain("**Editing**"); expect(guide).toContain("**Fullscreen mode (`/fullscreen`)**"); expect(guide).toContain("Queue follow-up message"); expect(guide).not.toContain("Ctrl+Z"); expect(guide).not.toContain("Suspend to background"); });
it("renders /hotkeys in chat history instead of the temporary guide", () => { const shortcutGuideContainer = new Container(); const chatContainer = new Container(); const mode = Object.assign(createMode(), { shortcutGuideContainer, chatContainer, ui: { requestRender: vi.fn() }, getMarkdownThemeWithSettings: () => getMarkdownTheme(), });
Reflect.get(InteractiveMode.prototype, "handleHotkeysCommand").call(mode);
expect(chatContainer.children).toHaveLength(2); expect(shortcutGuideContainer.children).toHaveLength(0); });});