@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -6,6 +6,8 @@ import type { Theme } from "../modes/theme/theme";
|
|
|
6
6
|
import type { ToolSession } from "../sdk";
|
|
7
7
|
import type { SessionEntry } from "../session/session-entries";
|
|
8
8
|
export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
|
|
9
|
+
/** Operation names accepted by the todo tool and echoed in successful result details. */
|
|
10
|
+
export type TodoOperation = "init" | "start" | "done" | "rm" | "drop" | "append" | "view";
|
|
9
11
|
export interface TodoItem {
|
|
10
12
|
content: string;
|
|
11
13
|
status: TodoStatus;
|
|
@@ -19,6 +21,8 @@ export interface TodoCompletionTransition {
|
|
|
19
21
|
content: string;
|
|
20
22
|
}
|
|
21
23
|
export interface TodoToolDetails {
|
|
24
|
+
/** Operation that produced this snapshot; absent on legacy transcript entries. */
|
|
25
|
+
op?: TodoOperation;
|
|
22
26
|
phases: TodoPhase[];
|
|
23
27
|
storage: "session" | "memory";
|
|
24
28
|
completedTasks?: TodoCompletionTransition[];
|
|
@@ -35,6 +39,8 @@ declare const todoSchema: import("arktype/internal/variants/object.ts").ObjectTy
|
|
|
35
39
|
}, {}>;
|
|
36
40
|
type TodoParams = TodoSchema;
|
|
37
41
|
type TodoSchema = typeof todoSchema.infer;
|
|
42
|
+
/** Return the active todo task, preferring an in-progress item over the first pending item. */
|
|
43
|
+
export declare function nextActionableTask(phases: readonly TodoPhase[]): TodoItem | undefined;
|
|
38
44
|
export declare const USER_TODO_EDIT_CUSTOM_TYPE = "user_todo_edit";
|
|
39
45
|
export declare function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPhase[];
|
|
40
46
|
/**
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Result submission tool for subagent output.
|
|
3
3
|
*
|
|
4
|
-
* Subagents
|
|
4
|
+
* Subagents can call this tool incrementally or terminally depending on `type`.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { TSchema } from "@oh-my-pi/pi-ai/types";
|
|
8
8
|
import type { ToolSession } from ".";
|
|
9
9
|
export interface YieldDetails {
|
|
10
|
-
|
|
10
|
+
/** Successful result payload, or omitted when `useLastTurn` requests last-turn extraction. */
|
|
11
|
+
data?: unknown;
|
|
11
12
|
status: "success" | "aborted";
|
|
12
13
|
error?: string;
|
|
14
|
+
/** Optional result section/classification supplied by the yield caller. */
|
|
15
|
+
type?: string | string[];
|
|
16
|
+
/** True when the caller intentionally omitted success data so the executor uses the last assistant turn. */
|
|
17
|
+
useLastTurn?: boolean;
|
|
13
18
|
/**
|
|
14
19
|
* Set when the yield tool exhausted its in-tool schema-retry budget
|
|
15
20
|
* (MAX_SCHEMA_RETRIES) and accepted the data anyway. Surfaced so the
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
1
2
|
export declare function canonicalizeMessage(text: string | null | undefined): string;
|
|
2
3
|
export declare function formatThinkingForDisplay(text: string, proseOnly: boolean): string;
|
|
4
|
+
/** Whether a formatted thinking block has non-placeholder content worth rendering. */
|
|
3
5
|
export declare function hasDisplayableThinking(text: string | null | undefined, formattedText: string | null | undefined): boolean;
|
|
6
|
+
/** Whether an assistant message contains thinking content the TUI can reveal. */
|
|
7
|
+
export declare function messageHasDisplayableThinking(message: AgentMessage, proseOnly: boolean): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -55,17 +55,17 @@
|
|
|
55
55
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
56
56
|
"@babel/parser": "^7.29.7",
|
|
57
57
|
"@mozilla/readability": "^0.6.0",
|
|
58
|
-
"@oh-my-pi/hashline": "16.2.
|
|
59
|
-
"@oh-my-pi/omp-stats": "16.2.
|
|
60
|
-
"@oh-my-pi/pi-agent-core": "16.2.
|
|
61
|
-
"@oh-my-pi/pi-ai": "16.2.
|
|
62
|
-
"@oh-my-pi/pi-catalog": "16.2.
|
|
63
|
-
"@oh-my-pi/pi-mnemopi": "16.2.
|
|
64
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
65
|
-
"@oh-my-pi/pi-tui": "16.2.
|
|
66
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
67
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
68
|
-
"@oh-my-pi/snapcompact": "16.2.
|
|
58
|
+
"@oh-my-pi/hashline": "16.2.3",
|
|
59
|
+
"@oh-my-pi/omp-stats": "16.2.3",
|
|
60
|
+
"@oh-my-pi/pi-agent-core": "16.2.3",
|
|
61
|
+
"@oh-my-pi/pi-ai": "16.2.3",
|
|
62
|
+
"@oh-my-pi/pi-catalog": "16.2.3",
|
|
63
|
+
"@oh-my-pi/pi-mnemopi": "16.2.3",
|
|
64
|
+
"@oh-my-pi/pi-natives": "16.2.3",
|
|
65
|
+
"@oh-my-pi/pi-tui": "16.2.3",
|
|
66
|
+
"@oh-my-pi/pi-utils": "16.2.3",
|
|
67
|
+
"@oh-my-pi/pi-wire": "16.2.3",
|
|
68
|
+
"@oh-my-pi/snapcompact": "16.2.3",
|
|
69
69
|
"@opentelemetry/api": "^1.9.1",
|
|
70
70
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
71
71
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "bun:test";
|
|
2
2
|
import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { TUI } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { type } from "arktype";
|
|
5
|
+
import type { ModelRegistry } from "../../config/model-registry";
|
|
6
|
+
import type { Settings } from "../../config/settings";
|
|
7
|
+
import { type AdvisorConfigDeps, AdvisorConfigOverlayComponent } from "../../modes/components/advisor-config";
|
|
4
8
|
import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
|
|
5
|
-
import { getThemeByName } from "../../modes/theme/theme";
|
|
9
|
+
import { getThemeByName, setThemeInstance } from "../../modes/theme/theme";
|
|
6
10
|
import advisorSystemPrompt from "../../prompts/advisor/system.md" with { type: "text" };
|
|
7
11
|
import { SecretObfuscator } from "../../secrets/obfuscator";
|
|
8
12
|
import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
|
|
9
13
|
import { YieldQueue } from "../../session/yield-queue";
|
|
14
|
+
import { BUILTIN_TOOL_NAMES } from "../../tools/builtin-names";
|
|
10
15
|
import {
|
|
11
|
-
|
|
16
|
+
ADVISOR_DEFAULT_TOOL_NAMES,
|
|
12
17
|
AdviseTool,
|
|
13
18
|
type AdvisorAgent,
|
|
14
19
|
type AdvisorNote,
|
|
15
20
|
AdvisorRuntime,
|
|
16
21
|
type AdvisorRuntimeHost,
|
|
22
|
+
advisorTranscriptFilename,
|
|
17
23
|
deriveAdvisorTelemetry,
|
|
18
24
|
formatAdvisorBatchContent,
|
|
19
25
|
formatAdvisorContextPrompt,
|
|
20
26
|
isAdvisorInterruptImmuneTurnActive,
|
|
27
|
+
isAdvisorTranscriptName,
|
|
21
28
|
isInterruptingSeverity,
|
|
22
29
|
resolveAdvisorDeliveryChannel,
|
|
30
|
+
type WatchdogConfigDoc,
|
|
23
31
|
} from "..";
|
|
24
32
|
|
|
25
33
|
describe("advisor", () => {
|
|
@@ -158,6 +166,57 @@ describe("advisor", () => {
|
|
|
158
166
|
});
|
|
159
167
|
});
|
|
160
168
|
|
|
169
|
+
describe("formatSessionHistoryMarkdown expandEditDiffs", () => {
|
|
170
|
+
const diff = "--- a/foo.ts\n+++ b/foo.ts\n@@ -1,2 +1,2 @@\n-const x = 1;\n+const x = 2;";
|
|
171
|
+
const editCall = {
|
|
172
|
+
role: "assistant",
|
|
173
|
+
content: [{ type: "toolCall", id: "c1", name: "edit", arguments: { path: "foo.ts" } }],
|
|
174
|
+
timestamp: 1,
|
|
175
|
+
} as unknown as AgentMessage;
|
|
176
|
+
const editResult = {
|
|
177
|
+
role: "toolResult",
|
|
178
|
+
toolCallId: "c1",
|
|
179
|
+
toolName: "edit",
|
|
180
|
+
content: "ok",
|
|
181
|
+
details: { diff },
|
|
182
|
+
timestamp: 2,
|
|
183
|
+
} as unknown as AgentMessage;
|
|
184
|
+
|
|
185
|
+
it("appends the full diff in a fenced block when expandEditDiffs is set", () => {
|
|
186
|
+
const md = formatSessionHistoryMarkdown([editCall, editResult], {
|
|
187
|
+
expandEditDiffs: true,
|
|
188
|
+
watchedRoles: true,
|
|
189
|
+
});
|
|
190
|
+
expect(md).toContain("```diff");
|
|
191
|
+
expect(md).toContain("-const x = 1;");
|
|
192
|
+
expect(md).toContain("+const x = 2;");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("omits the diff body without the flag", () => {
|
|
196
|
+
const md = formatSessionHistoryMarkdown([editCall, editResult], { watchedRoles: true });
|
|
197
|
+
expect(md).not.toContain("```diff");
|
|
198
|
+
expect(md).not.toContain("+const x = 2;");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("widens the fence past backtick runs in the diff body", () => {
|
|
202
|
+
const fenced = "--- a/readme.md\n+++ b/readme.md\n@@ -1 +1 @@\n-```\n+```ts\n+code\n+```";
|
|
203
|
+
const result = {
|
|
204
|
+
role: "toolResult",
|
|
205
|
+
toolCallId: "c1",
|
|
206
|
+
toolName: "edit",
|
|
207
|
+
content: "ok",
|
|
208
|
+
details: { diff: fenced },
|
|
209
|
+
timestamp: 2,
|
|
210
|
+
} as unknown as AgentMessage;
|
|
211
|
+
const md = formatSessionHistoryMarkdown([editCall, result], {
|
|
212
|
+
expandEditDiffs: true,
|
|
213
|
+
watchedRoles: true,
|
|
214
|
+
});
|
|
215
|
+
// The body contains a ``` run, so the wrapping fence widens to 4 backticks.
|
|
216
|
+
expect(md).toContain("````diff");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
161
220
|
describe("advisor yield-queue dispatcher", () => {
|
|
162
221
|
it("batches advice notes into one custom message", async () => {
|
|
163
222
|
const injected: AgentMessage[] = [];
|
|
@@ -373,6 +432,18 @@ describe("advisor", () => {
|
|
|
373
432
|
// Exactly one severity attribute (only the blocker note carries one).
|
|
374
433
|
expect(content.split('severity="').length - 1).toBe(1);
|
|
375
434
|
});
|
|
435
|
+
|
|
436
|
+
it("emits an advisor attribute only for named advisors, escaping the name", () => {
|
|
437
|
+
const content = formatAdvisorBatchContent([
|
|
438
|
+
{ note: "named note", advisor: 'Arch "X"' },
|
|
439
|
+
{ note: "default note" },
|
|
440
|
+
]);
|
|
441
|
+
// Named advisor: attribute present, double quote escaped for attribute context.
|
|
442
|
+
expect(content).toContain('advisor="Arch "X""');
|
|
443
|
+
// A note with no source (the legacy/default advisor) carries no advisor attribute.
|
|
444
|
+
expect(content.split('advisor="').length - 1).toBe(1);
|
|
445
|
+
expect(content).toContain("default note");
|
|
446
|
+
});
|
|
376
447
|
});
|
|
377
448
|
|
|
378
449
|
describe("deriveAdvisorTelemetry", () => {
|
|
@@ -662,6 +733,46 @@ describe("advisor", () => {
|
|
|
662
733
|
expect(promptInputs[0]).not.toContain(secret);
|
|
663
734
|
});
|
|
664
735
|
|
|
736
|
+
it("surfaces edit diff details but redacts secrets inside the diff", async () => {
|
|
737
|
+
const secret = "DIFF_SECRET_TOKEN_123";
|
|
738
|
+
const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
|
|
739
|
+
const placeholder = obfuscator.obfuscate(secret);
|
|
740
|
+
const promptInputs: string[] = [];
|
|
741
|
+
const agent = makeAgent(promptInputs);
|
|
742
|
+
const diff = `--- a/config.ts\n+++ b/config.ts\n@@ -1 +1 @@\n-const token = "old";\n+const token = "${secret}";`;
|
|
743
|
+
const messages: AgentMessage[] = [
|
|
744
|
+
{
|
|
745
|
+
role: "assistant",
|
|
746
|
+
content: [{ type: "toolCall", id: "c1", name: "edit", arguments: { path: "config.ts" } }],
|
|
747
|
+
timestamp: 1,
|
|
748
|
+
} as unknown as AgentMessage,
|
|
749
|
+
{
|
|
750
|
+
role: "toolResult",
|
|
751
|
+
toolCallId: "c1",
|
|
752
|
+
toolName: "edit",
|
|
753
|
+
content: "ok",
|
|
754
|
+
details: { diff },
|
|
755
|
+
timestamp: 2,
|
|
756
|
+
} as unknown as AgentMessage,
|
|
757
|
+
];
|
|
758
|
+
const host: AdvisorRuntimeHost = {
|
|
759
|
+
snapshotMessages: () => messages,
|
|
760
|
+
enqueueAdvice: () => {},
|
|
761
|
+
obfuscator,
|
|
762
|
+
};
|
|
763
|
+
const runtime = new AdvisorRuntime(agent, host);
|
|
764
|
+
|
|
765
|
+
runtime.onTurnEnd();
|
|
766
|
+
await Promise.resolve();
|
|
767
|
+
|
|
768
|
+
expect(promptInputs).toHaveLength(1);
|
|
769
|
+
// The diff is surfaced to the advisor (expandEditDiffs) ...
|
|
770
|
+
expect(promptInputs[0]).toContain("+const token =");
|
|
771
|
+
// ... but a secret living inside details.diff is obfuscated (details now walked).
|
|
772
|
+
expect(promptInputs[0]).toContain(placeholder);
|
|
773
|
+
expect(promptInputs[0]).not.toContain(secret);
|
|
774
|
+
});
|
|
775
|
+
|
|
665
776
|
it("expands plan-mode context once, then collapses an unchanged re-injection", async () => {
|
|
666
777
|
const promptInputs: string[] = [];
|
|
667
778
|
const agent = makeAgent(promptInputs);
|
|
@@ -1005,6 +1116,202 @@ describe("advisor", () => {
|
|
|
1005
1116
|
expect(runtime.backlog).toBe(0);
|
|
1006
1117
|
});
|
|
1007
1118
|
|
|
1119
|
+
it("notifies the host once when consecutive prompt failures make the advisor unavailable", async () => {
|
|
1120
|
+
const promptInputs: string[] = [];
|
|
1121
|
+
const failures: unknown[] = [];
|
|
1122
|
+
let shouldFail = true;
|
|
1123
|
+
const agent: AdvisorAgent = {
|
|
1124
|
+
prompt: async input => {
|
|
1125
|
+
promptInputs.push(input);
|
|
1126
|
+
if (shouldFail) {
|
|
1127
|
+
throw new Error("404 No endpoints available matching your guardrail restrictions and data policy.");
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
abort: () => {},
|
|
1131
|
+
reset: () => {},
|
|
1132
|
+
state: { messages: [] },
|
|
1133
|
+
};
|
|
1134
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1135
|
+
const host: AdvisorRuntimeHost = {
|
|
1136
|
+
snapshotMessages: () => messages,
|
|
1137
|
+
enqueueAdvice: () => {},
|
|
1138
|
+
notifyFailure: error => failures.push(error),
|
|
1139
|
+
};
|
|
1140
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1141
|
+
|
|
1142
|
+
runtime.onTurnEnd(messages);
|
|
1143
|
+
await Bun.sleep(0);
|
|
1144
|
+
await Bun.sleep(0);
|
|
1145
|
+
await Bun.sleep(0);
|
|
1146
|
+
|
|
1147
|
+
expect(promptInputs).toHaveLength(3);
|
|
1148
|
+
expect(failures).toHaveLength(1);
|
|
1149
|
+
const failure = failures[0];
|
|
1150
|
+
expect(failure).toBeInstanceOf(Error);
|
|
1151
|
+
if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
|
|
1152
|
+
expect(failure.message).toContain("No endpoints available");
|
|
1153
|
+
|
|
1154
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1155
|
+
runtime.onTurnEnd(messages);
|
|
1156
|
+
await Bun.sleep(0);
|
|
1157
|
+
await Bun.sleep(0);
|
|
1158
|
+
await Bun.sleep(0);
|
|
1159
|
+
|
|
1160
|
+
expect(promptInputs).toHaveLength(6);
|
|
1161
|
+
expect(failures).toHaveLength(1);
|
|
1162
|
+
|
|
1163
|
+
shouldFail = false;
|
|
1164
|
+
messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
|
|
1165
|
+
runtime.onTurnEnd(messages);
|
|
1166
|
+
await Bun.sleep(0);
|
|
1167
|
+
expect(failures).toHaveLength(1);
|
|
1168
|
+
|
|
1169
|
+
shouldFail = true;
|
|
1170
|
+
messages.push({ role: "user", content: "ddd", timestamp: 4 } as AgentMessage);
|
|
1171
|
+
runtime.onTurnEnd(messages);
|
|
1172
|
+
await Bun.sleep(0);
|
|
1173
|
+
await Bun.sleep(0);
|
|
1174
|
+
await Bun.sleep(0);
|
|
1175
|
+
|
|
1176
|
+
expect(failures).toHaveLength(2);
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it("treats a clean prompt resolution with state.error as a failed turn (real Agent contract)", async () => {
|
|
1180
|
+
// `Agent.#runLoop` catches provider/stream failures internally — it resolves
|
|
1181
|
+
// `prompt()` cleanly and stores the message on `state.error` (e.g. the
|
|
1182
|
+
// OpenRouter ZDR `404 No endpoints available` case from #3635). The runtime
|
|
1183
|
+
// must surface that as a failed turn even though the awaited promise did
|
|
1184
|
+
// not reject.
|
|
1185
|
+
const promptInputs: string[] = [];
|
|
1186
|
+
const failures: unknown[] = [];
|
|
1187
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1188
|
+
let shouldFail = true;
|
|
1189
|
+
const agent: AdvisorAgent = {
|
|
1190
|
+
prompt: async input => {
|
|
1191
|
+
promptInputs.push(input);
|
|
1192
|
+
state.error = shouldFail
|
|
1193
|
+
? "404 No endpoints available matching your guardrail restrictions and data policy."
|
|
1194
|
+
: undefined;
|
|
1195
|
+
},
|
|
1196
|
+
abort: () => {},
|
|
1197
|
+
reset: () => {
|
|
1198
|
+
state.error = undefined;
|
|
1199
|
+
},
|
|
1200
|
+
state,
|
|
1201
|
+
};
|
|
1202
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1203
|
+
const host: AdvisorRuntimeHost = {
|
|
1204
|
+
snapshotMessages: () => messages,
|
|
1205
|
+
enqueueAdvice: () => {},
|
|
1206
|
+
notifyFailure: error => failures.push(error),
|
|
1207
|
+
};
|
|
1208
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1209
|
+
|
|
1210
|
+
runtime.onTurnEnd(messages);
|
|
1211
|
+
await Bun.sleep(0);
|
|
1212
|
+
await Bun.sleep(0);
|
|
1213
|
+
await Bun.sleep(0);
|
|
1214
|
+
|
|
1215
|
+
expect(promptInputs).toHaveLength(3);
|
|
1216
|
+
expect(failures).toHaveLength(1);
|
|
1217
|
+
const failure = failures[0];
|
|
1218
|
+
if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
|
|
1219
|
+
expect(failure.message).toContain("No endpoints available");
|
|
1220
|
+
expect(runtime.backlog).toBe(0);
|
|
1221
|
+
|
|
1222
|
+
shouldFail = false;
|
|
1223
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1224
|
+
runtime.onTurnEnd(messages);
|
|
1225
|
+
await Bun.sleep(0);
|
|
1226
|
+
expect(failures).toHaveLength(1);
|
|
1227
|
+
|
|
1228
|
+
shouldFail = true;
|
|
1229
|
+
messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
|
|
1230
|
+
runtime.onTurnEnd(messages);
|
|
1231
|
+
await Bun.sleep(0);
|
|
1232
|
+
await Bun.sleep(0);
|
|
1233
|
+
await Bun.sleep(0);
|
|
1234
|
+
|
|
1235
|
+
expect(failures).toHaveLength(2);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it("rolls advisor state back after each failed prompt so retries don't replay duplicate turns", async () => {
|
|
1239
|
+
// The real `Agent` appends the user batch + a synthetic `stopReason: "error"`
|
|
1240
|
+
// assistant turn before `state.error` is read. Without rollback, the runtime's
|
|
1241
|
+
// retry/drop path would replay the failed batch on top of those orphans,
|
|
1242
|
+
// duplicating session-update user turns and leaking dropped failures into the
|
|
1243
|
+
// next successful run's context.
|
|
1244
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1245
|
+
const rollbackCalls: number[] = [];
|
|
1246
|
+
const lengthsBeforePrompt: number[] = [];
|
|
1247
|
+
let shouldFail = true;
|
|
1248
|
+
const agent: AdvisorAgent = {
|
|
1249
|
+
prompt: async input => {
|
|
1250
|
+
lengthsBeforePrompt.push(state.messages.length);
|
|
1251
|
+
state.messages.push({ role: "user", content: input, timestamp: Date.now() } as AgentMessage);
|
|
1252
|
+
if (shouldFail) {
|
|
1253
|
+
state.messages.push({
|
|
1254
|
+
role: "assistant",
|
|
1255
|
+
content: [{ type: "text", text: "" }],
|
|
1256
|
+
stopReason: "error",
|
|
1257
|
+
errorMessage: "404 No endpoints available",
|
|
1258
|
+
timestamp: Date.now(),
|
|
1259
|
+
} as unknown as AgentMessage);
|
|
1260
|
+
state.error = "404 No endpoints available";
|
|
1261
|
+
} else {
|
|
1262
|
+
state.messages.push({
|
|
1263
|
+
role: "assistant",
|
|
1264
|
+
content: [{ type: "text", text: "ok" }],
|
|
1265
|
+
timestamp: Date.now(),
|
|
1266
|
+
} as unknown as AgentMessage);
|
|
1267
|
+
state.error = undefined;
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
abort: () => {},
|
|
1271
|
+
reset: () => {
|
|
1272
|
+
state.messages.length = 0;
|
|
1273
|
+
state.error = undefined;
|
|
1274
|
+
},
|
|
1275
|
+
rollbackTo: count => {
|
|
1276
|
+
rollbackCalls.push(count);
|
|
1277
|
+
if (count < state.messages.length) state.messages.length = count;
|
|
1278
|
+
state.error = undefined;
|
|
1279
|
+
},
|
|
1280
|
+
state,
|
|
1281
|
+
};
|
|
1282
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1283
|
+
const host: AdvisorRuntimeHost = {
|
|
1284
|
+
snapshotMessages: () => messages,
|
|
1285
|
+
enqueueAdvice: () => {},
|
|
1286
|
+
};
|
|
1287
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1288
|
+
|
|
1289
|
+
runtime.onTurnEnd(messages);
|
|
1290
|
+
await Bun.sleep(0);
|
|
1291
|
+
await Bun.sleep(0);
|
|
1292
|
+
await Bun.sleep(0);
|
|
1293
|
+
|
|
1294
|
+
// Three failed prompts each rolled back to the empty baseline, so every retry
|
|
1295
|
+
// saw a clean state.messages instead of stacked failed turns.
|
|
1296
|
+
expect(lengthsBeforePrompt).toEqual([0, 0, 0]);
|
|
1297
|
+
expect(rollbackCalls).toEqual([0, 0, 0]);
|
|
1298
|
+
// The drop-after-3 path also left state.messages empty — no orphan failed
|
|
1299
|
+
// turns leak into the next successful run's context.
|
|
1300
|
+
expect(state.messages).toHaveLength(0);
|
|
1301
|
+
expect(state.error).toBeUndefined();
|
|
1302
|
+
|
|
1303
|
+
// A subsequent successful run starts from the clean baseline and is NOT
|
|
1304
|
+
// rolled back.
|
|
1305
|
+
shouldFail = false;
|
|
1306
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1307
|
+
runtime.onTurnEnd(messages);
|
|
1308
|
+
await Bun.sleep(0);
|
|
1309
|
+
|
|
1310
|
+
expect(lengthsBeforePrompt[lengthsBeforePrompt.length - 1]).toBe(0);
|
|
1311
|
+
expect(rollbackCalls).toHaveLength(3);
|
|
1312
|
+
expect(state.messages).toHaveLength(2);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1008
1315
|
it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
|
|
1009
1316
|
const promptInputs: string[] = [];
|
|
1010
1317
|
const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();
|
|
@@ -1063,14 +1370,18 @@ describe("advisor", () => {
|
|
|
1063
1370
|
});
|
|
1064
1371
|
});
|
|
1065
1372
|
|
|
1066
|
-
describe("
|
|
1067
|
-
it("
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1373
|
+
describe("advisor default tools", () => {
|
|
1374
|
+
it("defaults to read/grep/glob, a subset of the full grantable tool pool", () => {
|
|
1375
|
+
expect([...ADVISOR_DEFAULT_TOOL_NAMES]).toEqual(["read", "grep", "glob"]);
|
|
1376
|
+
// The advisor is a full agent now: every built tool is grantable (no hard
|
|
1377
|
+
// read-only restriction), including mutating ones like edit/bash/write.
|
|
1378
|
+
const builtin = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
1379
|
+
for (const name of ["read", "grep", "glob", "edit", "bash", "write"]) {
|
|
1380
|
+
expect(builtin.has(name)).toBe(true);
|
|
1381
|
+
}
|
|
1382
|
+
for (const name of ADVISOR_DEFAULT_TOOL_NAMES) {
|
|
1383
|
+
expect(builtin.has(name)).toBe(true);
|
|
1384
|
+
}
|
|
1074
1385
|
});
|
|
1075
1386
|
});
|
|
1076
1387
|
|
|
@@ -1093,6 +1404,26 @@ describe("advisor", () => {
|
|
|
1093
1404
|
expect(text).toContain("watch the empty case");
|
|
1094
1405
|
});
|
|
1095
1406
|
|
|
1407
|
+
it("prefixes the note with a named-advisor label, but not for the default advisor", async () => {
|
|
1408
|
+
const uiTheme = await getThemeByName("dark");
|
|
1409
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1410
|
+
const card = createAdvisorMessageCard(
|
|
1411
|
+
{
|
|
1412
|
+
notes: [
|
|
1413
|
+
{ note: "module boundary leak", severity: "concern", advisor: "Architecture" },
|
|
1414
|
+
{ note: "default-advisor note", advisor: "default" },
|
|
1415
|
+
],
|
|
1416
|
+
},
|
|
1417
|
+
() => true,
|
|
1418
|
+
uiTheme,
|
|
1419
|
+
);
|
|
1420
|
+
const text = strip(card.render(80));
|
|
1421
|
+
expect(text).toContain("[Architecture]");
|
|
1422
|
+
expect(text).toContain("module boundary leak");
|
|
1423
|
+
// The implicit "default" advisor stays unlabeled.
|
|
1424
|
+
expect(text).not.toContain("[default]");
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1096
1427
|
it("collapses to the first notes with an overflow hint", async () => {
|
|
1097
1428
|
const uiTheme = await getThemeByName("dark");
|
|
1098
1429
|
if (!uiTheme) throw new Error("theme unavailable");
|
|
@@ -1231,4 +1562,101 @@ describe("advisor", () => {
|
|
|
1231
1562
|
}
|
|
1232
1563
|
});
|
|
1233
1564
|
});
|
|
1565
|
+
describe("advisor transcript filenames", () => {
|
|
1566
|
+
it("derives default and named transcript filenames", () => {
|
|
1567
|
+
expect(advisorTranscriptFilename("")).toBe("__advisor.jsonl");
|
|
1568
|
+
expect(advisorTranscriptFilename("arch")).toBe("__advisor.arch.jsonl");
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
it("recognizes default and named advisor transcripts, and nothing else", () => {
|
|
1572
|
+
expect(isAdvisorTranscriptName("__advisor.jsonl")).toBe(true);
|
|
1573
|
+
expect(isAdvisorTranscriptName("__advisor.arch.jsonl")).toBe(true);
|
|
1574
|
+
expect(isAdvisorTranscriptName("__advisor-2.jsonl")).toBe(false);
|
|
1575
|
+
expect(isAdvisorTranscriptName("Foo.jsonl")).toBe(false);
|
|
1576
|
+
expect(isAdvisorTranscriptName("__advisor.arch.bak")).toBe(false);
|
|
1577
|
+
});
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
describe("AdvisorConfigOverlayComponent", () => {
|
|
1581
|
+
const deps = {
|
|
1582
|
+
modelRegistry: { getCanonicalModelSelections: () => [] } as unknown as ModelRegistry,
|
|
1583
|
+
settings: {} as unknown as Settings,
|
|
1584
|
+
scopedModels: [],
|
|
1585
|
+
availableToolNames: ["read", "grep", "glob", "lsp", "web_search"],
|
|
1586
|
+
};
|
|
1587
|
+
const callbacks = {
|
|
1588
|
+
loadDoc: async () => ({ advisors: [] }),
|
|
1589
|
+
save: async () => {},
|
|
1590
|
+
close: () => {},
|
|
1591
|
+
requestRender: () => {},
|
|
1592
|
+
notify: () => {},
|
|
1593
|
+
};
|
|
1594
|
+
const strip = (lines: readonly string[]): string => lines.join("\n").replace(/\x1b\[[0-9;]*m/g, "");
|
|
1595
|
+
const make = (doc: WatchdogConfigDoc, extra?: Partial<AdvisorConfigDeps>): AdvisorConfigOverlayComponent =>
|
|
1596
|
+
new AdvisorConfigOverlayComponent({} as unknown as TUI, { ...deps, ...extra }, "project", doc, callbacks);
|
|
1597
|
+
const fullHeight = Math.max(14, process.stdout.rows || 40);
|
|
1598
|
+
|
|
1599
|
+
it("paints a full-screen split frame: roster sidebar + selected-advisor preview", async () => {
|
|
1600
|
+
const uiTheme = await getThemeByName("dark");
|
|
1601
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1602
|
+
setThemeInstance(uiTheme);
|
|
1603
|
+
const overlay = make({
|
|
1604
|
+
instructions: "shared baseline",
|
|
1605
|
+
advisors: [
|
|
1606
|
+
{ name: "Architecture", model: "x-ai/grok-code-fast:high" },
|
|
1607
|
+
{ name: "Security", tools: ["read", "web_search"] },
|
|
1608
|
+
],
|
|
1609
|
+
});
|
|
1610
|
+
const frame = overlay.render(200);
|
|
1611
|
+
// Fills the screen top-to-bottom (the fix for the bottom-anchored frame
|
|
1612
|
+
// whose offset broke mouse hit-testing and wasted the upper space).
|
|
1613
|
+
expect(frame.length).toBe(fullHeight);
|
|
1614
|
+
const text = strip(frame);
|
|
1615
|
+
expect(text).toContain("Advisor configuration");
|
|
1616
|
+
expect(text).toContain("project");
|
|
1617
|
+
expect(text).toContain("Architecture");
|
|
1618
|
+
expect(text).toContain("Security");
|
|
1619
|
+
expect(text).toContain("+ Add advisor");
|
|
1620
|
+
expect(text).toContain("Save & apply");
|
|
1621
|
+
// Right preview reflects the highlighted (first) advisor.
|
|
1622
|
+
expect(text).toContain("x-ai/grok-code-fast:high");
|
|
1623
|
+
expect(text).toContain("read, grep, glob (default)");
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
it("moves the preview with keyboard selection and preserves an explicit tool set", async () => {
|
|
1627
|
+
const uiTheme = await getThemeByName("dark");
|
|
1628
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1629
|
+
setThemeInstance(uiTheme);
|
|
1630
|
+
const overlay = make({
|
|
1631
|
+
advisors: [{ name: "Architecture" }, { name: "Security", tools: ["read", "web_search"] }],
|
|
1632
|
+
});
|
|
1633
|
+
overlay.render(200);
|
|
1634
|
+
overlay.handleInput("\x1b[B"); // arrow down → highlight Security
|
|
1635
|
+
expect(strip(overlay.render(200))).toContain("read, web_search");
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
it("opens an advisor's detail editor on a left click in the sidebar", async () => {
|
|
1639
|
+
const uiTheme = await getThemeByName("dark");
|
|
1640
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1641
|
+
setThemeInstance(uiTheme);
|
|
1642
|
+
const overlay = make({ advisors: [{ name: "Architecture" }, { name: "Security" }] });
|
|
1643
|
+
// Render once so the frame geometry is recorded; the first advisor sits on
|
|
1644
|
+
// the first body row (0-based screen row 1 → SGR 1-based row 2).
|
|
1645
|
+
overlay.render(120);
|
|
1646
|
+
overlay.handleInput("\x1b[<0;4;2M"); // left-button press, col 4, row 2
|
|
1647
|
+
const text = strip(overlay.render(120));
|
|
1648
|
+
expect(text).toContain("Editing");
|
|
1649
|
+
expect(text).toContain("Architecture");
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
it("seeds a visible default advisor (labeled with the role model) when the config is empty", async () => {
|
|
1653
|
+
const uiTheme = await getThemeByName("dark");
|
|
1654
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1655
|
+
setThemeInstance(uiTheme);
|
|
1656
|
+
const overlay = make({ advisors: [] }, { defaultModelLabel: "anthropic/claude-opus" });
|
|
1657
|
+
const text = strip(overlay.render(200));
|
|
1658
|
+
expect(text).toContain("default");
|
|
1659
|
+
expect(text).toContain("anthropic/claude-opus");
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1234
1662
|
});
|