@oh-my-pi/pi-coding-agent 16.2.13 → 16.3.0
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 +108 -7
- package/dist/cli.js +6033 -5982
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
package/src/lsp/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { isEnoent, logger, ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { isEnoent, logger, postmortem, ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { MessageFramer } from "../jsonrpc/message-framing";
|
|
4
4
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
5
5
|
import { applyWorkspaceEdit } from "./edits";
|
|
@@ -1232,21 +1232,18 @@ export function getActiveClients(): LspServerStatus[] {
|
|
|
1232
1232
|
// Process Cleanup
|
|
1233
1233
|
// =============================================================================
|
|
1234
1234
|
|
|
1235
|
-
//
|
|
1235
|
+
// Route signal-triggered LSP cleanup through the shared `postmortem` cleanup
|
|
1236
|
+
// list so it runs alongside every other session teardown (draft save,
|
|
1237
|
+
// `session.dispose()`, kernels, MCP) instead of racing them via a
|
|
1238
|
+
// module-owned `SIGINT`/`SIGTERM` handler + `process.exit(0)`. Historically
|
|
1239
|
+
// this file registered its own signal handlers that called `shutdownAll()`
|
|
1240
|
+
// then `process.exit(0)` — winning the race would drop `session_shutdown`
|
|
1241
|
+
// extensions, orphan background bash/task jobs, and skip the editor draft
|
|
1242
|
+
// save (issue #4080). `beforeExit` stays as-is: it fires only when the event
|
|
1243
|
+
// loop drains with no more work, distinct from signal delivery.
|
|
1236
1244
|
if (typeof process !== "undefined") {
|
|
1237
1245
|
process.on("beforeExit", () => {
|
|
1238
1246
|
void shutdownAll();
|
|
1239
1247
|
});
|
|
1240
|
-
|
|
1241
|
-
void (async () => {
|
|
1242
|
-
await shutdownAll();
|
|
1243
|
-
process.exit(0);
|
|
1244
|
-
})();
|
|
1245
|
-
});
|
|
1246
|
-
process.on("SIGTERM", () => {
|
|
1247
|
-
void (async () => {
|
|
1248
|
-
await shutdownAll();
|
|
1249
|
-
process.exit(0);
|
|
1250
|
-
})();
|
|
1251
|
-
});
|
|
1248
|
+
postmortem.register("lsp-shutdown", () => shutdownAll());
|
|
1252
1249
|
}
|
package/src/main.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
} from "./sdk";
|
|
67
67
|
import type { AgentSession } from "./session/agent-session";
|
|
68
68
|
import type { AuthStorage } from "./session/auth-storage";
|
|
69
|
+
import { describePendingToolCalls } from "./session/exit-diagnostics";
|
|
69
70
|
import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
|
|
70
71
|
import { SessionManager } from "./session/session-manager";
|
|
71
72
|
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
@@ -73,7 +74,7 @@ import { shouldShowStartupSplash } from "./startup-splash";
|
|
|
73
74
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
|
|
74
75
|
import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
|
|
75
76
|
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
76
|
-
import {
|
|
77
|
+
import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
|
|
77
78
|
import type { LspStartupServerInfo } from "./tools";
|
|
78
79
|
import {
|
|
79
80
|
getChangelogPath,
|
|
@@ -361,6 +362,13 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
|
|
361
362
|
const nextSettings = await args.settings.cloneForCwd(cwd);
|
|
362
363
|
const nextSessionManager = SessionManager.create(cwd, args.sessionDir);
|
|
363
364
|
const agentId = `acp:${nextSessionManager.getSessionId()}`;
|
|
365
|
+
// `baseOptions.titleSystemPrompt` is resolved from the launch cwd; an ACP
|
|
366
|
+
// host can open `session/new` for any client-supplied workspace, so
|
|
367
|
+
// re-discover `TITLE_SYSTEM.md` against THIS session's `cwd` to keep the
|
|
368
|
+
// replan-driven title refresh consistent with the target project's
|
|
369
|
+
// policy (PR #3736 follow-up).
|
|
370
|
+
const titleSystemPromptSource = discoverTitleSystemPromptFile(cwd);
|
|
371
|
+
const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
|
|
364
372
|
const { session: nextSession } = await args.createSession({
|
|
365
373
|
...args.baseOptions,
|
|
366
374
|
cwd,
|
|
@@ -371,6 +379,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
|
|
371
379
|
agentId,
|
|
372
380
|
hasUI: false,
|
|
373
381
|
enableMCP: false,
|
|
382
|
+
titleSystemPrompt,
|
|
374
383
|
});
|
|
375
384
|
if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
|
|
376
385
|
args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
|
|
@@ -396,7 +405,6 @@ async function runInteractiveMode(
|
|
|
396
405
|
eventBus?: EventBus,
|
|
397
406
|
initialMessage?: string,
|
|
398
407
|
initialImages?: ImageContent[],
|
|
399
|
-
titleSystemPrompt?: string,
|
|
400
408
|
joinLink?: string,
|
|
401
409
|
): Promise<void> {
|
|
402
410
|
const mode = new InteractiveMode(
|
|
@@ -407,7 +415,6 @@ async function runInteractiveMode(
|
|
|
407
415
|
lspServers,
|
|
408
416
|
mcpManager,
|
|
409
417
|
eventBus,
|
|
410
|
-
titleSystemPrompt,
|
|
411
418
|
);
|
|
412
419
|
|
|
413
420
|
// Cold-launch gate: the full setup wizard (every scene + the overlay and
|
|
@@ -785,7 +792,7 @@ async function buildSessionOptions(
|
|
|
785
792
|
sessionManager: SessionManager | undefined,
|
|
786
793
|
modelRegistry: ModelRegistry,
|
|
787
794
|
activeSettings: Settings,
|
|
788
|
-
): Promise<
|
|
795
|
+
): Promise<CreateAgentSessionOptions> {
|
|
789
796
|
const options: CreateAgentSessionOptions = {
|
|
790
797
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
791
798
|
autoApprove: parsed.autoApprove ?? false,
|
|
@@ -887,9 +894,9 @@ async function buildSessionOptions(
|
|
|
887
894
|
if (scopedModels.length > 0) {
|
|
888
895
|
// `auto` is a session-level concept only; per-scoped-model (Ctrl+P) thinking
|
|
889
896
|
// overrides stay concrete, so coerce the auto default to "unset" here.
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
897
|
+
const defaultThinkingLevel = concreteThinkingLevel(
|
|
898
|
+
parseConfiguredThinkingLevel(activeSettings.get("defaultThinkingLevel")),
|
|
899
|
+
);
|
|
893
900
|
options.scopedModels = scopedModels.map(scopedModel => ({
|
|
894
901
|
model: scopedModel.model,
|
|
895
902
|
thinkingLevel: scopedModel.explicitThinkingLevel
|
|
@@ -903,6 +910,13 @@ async function buildSessionOptions(
|
|
|
903
910
|
|
|
904
911
|
// System prompt
|
|
905
912
|
applyResolvedSystemPromptInputs(options, resolvedSystemPrompt, resolvedAppendPrompt);
|
|
913
|
+
// Replan-driven title refresh resolves the override from this same field on
|
|
914
|
+
// `AgentSession`, so threading it through `CreateAgentSessionOptions` keeps
|
|
915
|
+
// both first-input titling (`input-controller.ts`) and replan refresh
|
|
916
|
+
// (`AgentSession.#refreshTitleAfterReplan`) on one source of truth.
|
|
917
|
+
if (titleSystemPrompt) {
|
|
918
|
+
options.titleSystemPrompt = titleSystemPrompt;
|
|
919
|
+
}
|
|
906
920
|
|
|
907
921
|
// Tools
|
|
908
922
|
if (parsed.noTools) {
|
|
@@ -939,7 +953,7 @@ async function buildSessionOptions(
|
|
|
939
953
|
options.additionalExtensionPaths = [];
|
|
940
954
|
}
|
|
941
955
|
|
|
942
|
-
return
|
|
956
|
+
return options;
|
|
943
957
|
}
|
|
944
958
|
|
|
945
959
|
interface RunRootCommandDependencies {
|
|
@@ -1195,6 +1209,21 @@ export async function runRootCommand(
|
|
|
1195
1209
|
sessionManager = await SessionManager.open(selected.path);
|
|
1196
1210
|
}
|
|
1197
1211
|
|
|
1212
|
+
if (sessionManager && (parsedArgs.continue || parsedArgs.resume || parsedArgs.fork)) {
|
|
1213
|
+
const pendingToolWarning = describePendingToolCalls(sessionManager.getBranch());
|
|
1214
|
+
if (pendingToolWarning) {
|
|
1215
|
+
logger.warn("Resumed session has pending tool calls", {
|
|
1216
|
+
sessionId: sessionManager.getSessionId(),
|
|
1217
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
1218
|
+
});
|
|
1219
|
+
if (isInteractive) {
|
|
1220
|
+
notifs.push({ kind: "warn", message: pendingToolWarning });
|
|
1221
|
+
} else {
|
|
1222
|
+
process.stderr.write(`${chalk.yellow(`${pendingToolWarning}\n`)}`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1198
1227
|
await pluginPreloadPromise;
|
|
1199
1228
|
|
|
1200
1229
|
scheduleMarketplaceAutoUpdate({
|
|
@@ -1203,7 +1232,7 @@ export async function runRootCommand(
|
|
|
1203
1232
|
clearPluginRootsCache: clearPluginRootsAndCaches,
|
|
1204
1233
|
});
|
|
1205
1234
|
|
|
1206
|
-
const
|
|
1235
|
+
const sessionOptions = await logger.time(
|
|
1207
1236
|
"buildSessionOptions",
|
|
1208
1237
|
buildSessionOptions,
|
|
1209
1238
|
parsedArgs,
|
|
@@ -1410,7 +1439,6 @@ export async function runRootCommand(
|
|
|
1410
1439
|
eventBus,
|
|
1411
1440
|
initialMessage,
|
|
1412
1441
|
initialImages,
|
|
1413
|
-
titleSystemPrompt,
|
|
1414
1442
|
parsedArgs.join,
|
|
1415
1443
|
);
|
|
1416
1444
|
} else {
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -433,19 +433,55 @@ export class MnemopiSessionState {
|
|
|
433
433
|
* e.g. `mnemopiBackend.clear` — pass `{ consolidate: false }` to skip the
|
|
434
434
|
* extraction/sleep pass, since spending tokens on memories that will be
|
|
435
435
|
* wiped on the next line is wasted work (PR #2327 review).
|
|
436
|
+
*
|
|
437
|
+
* `timeoutMs` caps how long the consolidate await blocks the caller
|
|
438
|
+
* (the user-visible `/quit` / `/exit` shutdown path passes this so
|
|
439
|
+
* dispose returns within a UX budget — issue #3641). When the cap is
|
|
440
|
+
* hit, dispose returns immediately and detaches the still-in-flight
|
|
441
|
+
* consolidate; the SQLite handles are closed in the background once
|
|
442
|
+
* the consolidate settles so writes never race a closed handle, and
|
|
443
|
+
* any pending embeddings are SIGKILL'd along with the embed worker
|
|
444
|
+
* (a tolerable loss — working memory rows are durable; only the
|
|
445
|
+
* episodic promotion / embedding for the LAST few turns is skipped,
|
|
446
|
+
* and `maybeRetainOnAgentEnd` has already retained earlier turns).
|
|
436
447
|
*/
|
|
437
|
-
async dispose(options: { consolidate?: boolean } = {}): Promise<void> {
|
|
448
|
+
async dispose(options: { consolidate?: boolean; timeoutMs?: number } = {}): Promise<void> {
|
|
438
449
|
this.unsubscribe?.();
|
|
439
450
|
this.unsubscribe = undefined;
|
|
440
451
|
if (this.aliasOf) return;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
452
|
+
const closeOwned = (): void => {
|
|
453
|
+
for (const memory of this.scoped.owned) memory.close();
|
|
454
|
+
};
|
|
455
|
+
if (options.consolidate === false) {
|
|
456
|
+
closeOwned();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const consolidatePromise = this.consolidate().catch((error: unknown) => {
|
|
460
|
+
logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
|
|
461
|
+
});
|
|
462
|
+
const { timeoutMs } = options;
|
|
463
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
464
|
+
const TIMED_OUT = Symbol("mnemopi.dispose.timedOut");
|
|
465
|
+
const winner = await Promise.race([
|
|
466
|
+
consolidatePromise.then(() => undefined as unknown),
|
|
467
|
+
Bun.sleep(timeoutMs).then(() => TIMED_OUT as unknown),
|
|
468
|
+
]);
|
|
469
|
+
if (winner === TIMED_OUT) {
|
|
470
|
+
logger.warn("Mnemopi: consolidate-on-dispose exceeded shutdown budget; detaching to background.", {
|
|
471
|
+
timeoutMs,
|
|
472
|
+
});
|
|
473
|
+
// Defer close until the in-flight consolidate settles so SQLite
|
|
474
|
+
// writes don't race a closed handle. The process is on the way
|
|
475
|
+
// to `postmortem.quit(0)`; if it exits first, the OS reclaims
|
|
476
|
+
// the handles (and a still-pending embed() goes down with the
|
|
477
|
+
// embed worker the caller is about to SIGKILL).
|
|
478
|
+
void consolidatePromise.finally(closeOwned);
|
|
479
|
+
return;
|
|
446
480
|
}
|
|
481
|
+
} else {
|
|
482
|
+
await consolidatePromise;
|
|
447
483
|
}
|
|
448
|
-
|
|
484
|
+
closeOwned();
|
|
449
485
|
}
|
|
450
486
|
}
|
|
451
487
|
|
|
@@ -842,7 +842,7 @@ export class AcpAgent implements Agent {
|
|
|
842
842
|
if (!skill) {
|
|
843
843
|
return false;
|
|
844
844
|
}
|
|
845
|
-
const built = await buildSkillPromptMessage(skill, parsed.args);
|
|
845
|
+
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
|
846
846
|
await record.session.promptCustomMessage({
|
|
847
847
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
848
848
|
content: built.message,
|
|
@@ -130,13 +130,21 @@ function registerPersistedSubagentsFromDir(registry: AgentRegistry, dir: string,
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** Result of one host-backed transcript read for the Agent Hub viewer. */
|
|
134
|
+
export interface AgentHubRemoteTranscript {
|
|
135
|
+
text: string;
|
|
136
|
+
newSize: number;
|
|
137
|
+
/** Terminal read failure reported by the host; guests should surface it instead of retrying hot. */
|
|
138
|
+
error?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
/** Guest-side proxy for hub actions executed on the collab host. */
|
|
134
142
|
export interface AgentHubRemote {
|
|
135
143
|
chat(id: string, text: string): void;
|
|
136
144
|
kill(id: string): void;
|
|
137
145
|
revive(id: string): void;
|
|
138
|
-
/** Mirrors readFileIncremental: text from fromByte (complete JSONL lines), newSize = next fromByte base; null = unavailable. */
|
|
139
|
-
readTranscript(id: string, fromByte: number): Promise<
|
|
146
|
+
/** Mirrors readFileIncremental: text from fromByte (complete JSONL lines), newSize = next fromByte base; null = temporarily unavailable. */
|
|
147
|
+
readTranscript(id: string, fromByte: number): Promise<AgentHubRemoteTranscript | null>;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
export interface AgentHubDeps {
|
|
@@ -23,6 +23,7 @@ import type { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
|
23
23
|
import type { AgentRegistry, AgentStatus } from "../../registry/agent-registry";
|
|
24
24
|
import type { FileEntry, SessionMessageEntry } from "../../session/session-entries";
|
|
25
25
|
import { parseSessionEntries } from "../../session/session-loader";
|
|
26
|
+
import { replaceTabs, shortenPath, truncateToWidth } from "../../tools/render-utils";
|
|
26
27
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
27
28
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
28
29
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
@@ -61,6 +62,18 @@ const POLL_MS = 250;
|
|
|
61
62
|
|
|
62
63
|
const SENTINEL_BYTES = 4096;
|
|
63
64
|
|
|
65
|
+
/** Sanitize wire-delivered error text for a single TUI row: tabs → spaces,
|
|
66
|
+
* newlines collapsed, absolute paths shortened, truncated to `maxWidth`.
|
|
67
|
+
* `#remoteError` arrives as `String(err)` from the host — it can carry
|
|
68
|
+
* multi-line stacks and absolute host paths that would break the frame's
|
|
69
|
+
* 1-row accounting and leak host filesystem layout to guests. */
|
|
70
|
+
function sanitizeErrorLine(text: string, maxWidth: number): string {
|
|
71
|
+
const singleLine = replaceTabs(text)
|
|
72
|
+
.replace(/[\r\n]+/g, " ")
|
|
73
|
+
.replace(/\/[^\s'")\]]+/g, p => shortenPath(p));
|
|
74
|
+
return truncateToWidth(singleLine, Math.max(10, maxWidth));
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
interface LocalTranscriptSentinel {
|
|
65
78
|
offset: number;
|
|
66
79
|
bytes: Buffer;
|
|
@@ -137,6 +150,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
137
150
|
#remoteFetchInFlight = false;
|
|
138
151
|
#remoteToken = 0;
|
|
139
152
|
#remoteUnavailable = false;
|
|
153
|
+
#remoteError = "";
|
|
140
154
|
#hasRemoteData = false;
|
|
141
155
|
|
|
142
156
|
#model: string | undefined;
|
|
@@ -177,14 +191,17 @@ export class AgentTranscriptViewer implements Component {
|
|
|
177
191
|
|
|
178
192
|
dispose(): void {
|
|
179
193
|
this.#disposed = true;
|
|
180
|
-
|
|
181
|
-
clearInterval(this.#pollTimer);
|
|
182
|
-
this.#pollTimer = undefined;
|
|
183
|
-
}
|
|
194
|
+
this.#stopPolling();
|
|
184
195
|
this.#remoteToken++;
|
|
185
196
|
this.#builder.dispose();
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
#stopPolling(): void {
|
|
200
|
+
if (!this.#pollTimer) return;
|
|
201
|
+
clearInterval(this.#pollTimer);
|
|
202
|
+
this.#pollTimer = undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
188
205
|
// ========================================================================
|
|
189
206
|
// Transcript loading
|
|
190
207
|
// ========================================================================
|
|
@@ -345,11 +362,20 @@ export class AgentTranscriptViewer implements Component {
|
|
|
345
362
|
}
|
|
346
363
|
return;
|
|
347
364
|
}
|
|
365
|
+
if (result.error) {
|
|
366
|
+
this.#remoteError = result.error;
|
|
367
|
+
this.#hasRemoteData = true;
|
|
368
|
+
this.#remoteUnavailable = false;
|
|
369
|
+
this.#stopPolling();
|
|
370
|
+
this.deps.requestRender();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
348
373
|
if (result.newSize < fromByte) {
|
|
349
374
|
// Host transcript rotated/truncated — drop the stale rendered rows
|
|
350
375
|
// before restarting; otherwise the post-rotation fetch would stack
|
|
351
376
|
// new content under the pre-rotation history.
|
|
352
377
|
this.#remoteBytes = 0;
|
|
378
|
+
this.#remoteError = "";
|
|
353
379
|
this.#hasRemoteData = false;
|
|
354
380
|
this.#model = undefined;
|
|
355
381
|
this.#rebuild([]);
|
|
@@ -357,6 +383,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
357
383
|
return;
|
|
358
384
|
}
|
|
359
385
|
this.#remoteUnavailable = false;
|
|
386
|
+
this.#remoteError = "";
|
|
360
387
|
const firstData = !this.#hasRemoteData;
|
|
361
388
|
this.#hasRemoteData = true;
|
|
362
389
|
const lastNewline = result.text.lastIndexOf("\n");
|
|
@@ -534,7 +561,11 @@ export class AgentTranscriptViewer implements Component {
|
|
|
534
561
|
|
|
535
562
|
const headerLines = this.#headerLines(ref?.status, ref?.kind, ref?.parentId);
|
|
536
563
|
const footerLines = this.#footerLines();
|
|
537
|
-
const noticeLine = this.#notice
|
|
564
|
+
const noticeLine = this.#notice
|
|
565
|
+
? ` ${theme.fg("error", sanitizeErrorLine(this.#notice, innerWidth))}`
|
|
566
|
+
: this.#remoteError && !this.#builder.isEmpty
|
|
567
|
+
? ` ${theme.fg("error", sanitizeErrorLine(this.#remoteError, innerWidth))}`
|
|
568
|
+
: undefined;
|
|
538
569
|
const editorLines = this.#editor ? this.#editor.render(innerWidth) : [];
|
|
539
570
|
|
|
540
571
|
// Chrome: top border + header rows + divider border + (notice) + editor + footer + bottom border.
|
|
@@ -542,7 +573,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
542
573
|
const viewportHeight = Math.max(3, termHeight - chrome);
|
|
543
574
|
|
|
544
575
|
const contentLines = this.#builder.isEmpty
|
|
545
|
-
? [` ${theme.fg("dim", this.#placeholder())}`]
|
|
576
|
+
? [` ${theme.fg("dim", this.#placeholder(Math.max(10, contentWidth - 1)))}`]
|
|
546
577
|
: this.#builder.container.render(contentWidth);
|
|
547
578
|
this.#scrollView.setLines(contentLines);
|
|
548
579
|
this.#scrollView.setHeight(viewportHeight);
|
|
@@ -606,8 +637,9 @@ export class AgentTranscriptViewer implements Component {
|
|
|
606
637
|
return parts.join(theme.sep.dot);
|
|
607
638
|
}
|
|
608
639
|
|
|
609
|
-
#placeholder(): string {
|
|
640
|
+
#placeholder(maxWidth: number): string {
|
|
610
641
|
if (this.deps.remote) {
|
|
642
|
+
if (this.#remoteError) return sanitizeErrorLine(this.#remoteError, maxWidth);
|
|
611
643
|
if (this.#remoteUnavailable) return "Transcript lives on the host — not available.";
|
|
612
644
|
return this.#hasRemoteData ? "No messages yet." : "Loading transcript from host…";
|
|
613
645
|
}
|
|
@@ -34,11 +34,16 @@ type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type:
|
|
|
34
34
|
type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
|
|
35
35
|
|
|
36
36
|
function resolveThinkingDisplay(block: ThinkingContentBlock, proseOnly: boolean): { text: string; visible: boolean } {
|
|
37
|
-
const rawThinking = (block as DisplayThinkingContentBlock).rawThinking
|
|
38
|
-
|
|
37
|
+
const rawThinking = (block as DisplayThinkingContentBlock).rawThinking;
|
|
38
|
+
// When rawThinking is set, `block.thinking` is already the formatted display
|
|
39
|
+
// text that buildDisplayMessage produced (then revealed/sliced by the
|
|
40
|
+
// streaming controller) — re-running the formatter would double-process it,
|
|
41
|
+
// and the growing revealed slice would never hit the per-tick memo. Only
|
|
42
|
+
// format raw (non-display) thinking blocks.
|
|
43
|
+
const formatted = rawThinking !== undefined ? block.thinking : formatThinkingForDisplay(block.thinking, proseOnly);
|
|
39
44
|
return {
|
|
40
45
|
text: formatted.trim(),
|
|
41
|
-
visible: hasDisplayableThinking(rawThinking, formatted),
|
|
46
|
+
visible: hasDisplayableThinking(rawThinking ?? block.thinking, formatted),
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -329,17 +334,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
329
334
|
#thinkingDotsLabel(): string {
|
|
330
335
|
const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
|
|
331
336
|
const coloredGlyph = theme.fg("thinkingText", glyph);
|
|
337
|
+
const thinkingLabel = theme.fg("muted", " Thinking");
|
|
332
338
|
const rate = Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed());
|
|
333
339
|
// The numeric badge ("<total> · <rate> toks/s") only renders while this block
|
|
334
340
|
// is genuinely streaming provider tokens. A block that has observed no token
|
|
335
341
|
// delta (e.g. a provider that reports usage only at turn end) or whose rate
|
|
336
|
-
// has decayed to zero (a streaming lull) drops it entirely — the
|
|
337
|
-
//
|
|
338
|
-
// the session-wide gauge from leaking a previous
|
|
339
|
-
// token-less block.
|
|
340
|
-
if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph;
|
|
342
|
+
// has decayed to zero (a streaming lull) drops it entirely — the persistent
|
|
343
|
+
// text label keeps the pulse descriptive for terminals and screen readers.
|
|
344
|
+
// The liveness flag also stops the session-wide gauge from leaking a previous
|
|
345
|
+
// turn's rate onto a fresh token-less block.
|
|
346
|
+
if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph + thinkingLabel;
|
|
341
347
|
// Total provider tokens, dimmed, sit next to the pulse.
|
|
342
|
-
const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` ${formatNumber(this.#thinkingTokens)}`) : "";
|
|
348
|
+
const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` · ${formatNumber(this.#thinkingTokens)}`) : "";
|
|
343
349
|
// Speed badge color: dim gray at rest, brightening toward the theme accent as
|
|
344
350
|
// streaming speed climbs (gray → bright accent). Ease (sqrt) so typical
|
|
345
351
|
// mid-stream rates already read as clearly accent-tinted instead of staying
|
|
@@ -348,7 +354,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
348
354
|
const hex = lerpHex(theme.getColorHex("dim"), theme.getAccentColorHex(), ratio);
|
|
349
355
|
const rateText = ` · ${rate.toFixed(1)} toks/s`;
|
|
350
356
|
const rateSpan = theme.getColorMode() === "truecolor" ? chalk.hex(hex)(rateText) : theme.fg("muted", rateText);
|
|
351
|
-
return coloredGlyph + totalSpan + rateSpan;
|
|
357
|
+
return coloredGlyph + thinkingLabel + totalSpan + rateSpan;
|
|
352
358
|
}
|
|
353
359
|
|
|
354
360
|
#startThinkingAnimation(): void {
|
|
@@ -81,6 +81,8 @@ export class ChatTranscriptBuilder {
|
|
|
81
81
|
#readArgs = new Map<string, Record<string, unknown>>();
|
|
82
82
|
#readGroup: ReadToolGroupComponent | null = null;
|
|
83
83
|
#pendingUsage: Usage | undefined;
|
|
84
|
+
#pendingUsageDuration: number | undefined;
|
|
85
|
+
#pendingUsageTtft: number | undefined;
|
|
84
86
|
#lastAssistantUsage: Usage | undefined;
|
|
85
87
|
#waitingPoll: ToolExecutionComponent | null = null;
|
|
86
88
|
#todoSnapshot: ToolExecutionComponent | null = null;
|
|
@@ -127,6 +129,8 @@ export class ChatTranscriptBuilder {
|
|
|
127
129
|
this.#readArgs.clear();
|
|
128
130
|
this.#readGroup = null;
|
|
129
131
|
this.#pendingUsage = undefined;
|
|
132
|
+
this.#pendingUsageDuration = undefined;
|
|
133
|
+
this.#pendingUsageTtft = undefined;
|
|
130
134
|
this.#lastAssistantUsage = undefined;
|
|
131
135
|
this.#waitingPoll = null;
|
|
132
136
|
this.#todoSnapshot = null;
|
|
@@ -192,8 +196,12 @@ export class ChatTranscriptBuilder {
|
|
|
192
196
|
if (!this.#pendingUsage) return;
|
|
193
197
|
this.#readGroup?.seal();
|
|
194
198
|
this.#readGroup = null;
|
|
195
|
-
this.container.addChild(
|
|
199
|
+
this.container.addChild(
|
|
200
|
+
createUsageRowBlock(this.#pendingUsage, this.#pendingUsageDuration, this.#pendingUsageTtft),
|
|
201
|
+
);
|
|
196
202
|
this.#pendingUsage = undefined;
|
|
203
|
+
this.#pendingUsageDuration = undefined;
|
|
204
|
+
this.#pendingUsageTtft = undefined;
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
#appendChatMessage(message: AgentMessage): void {
|
|
@@ -343,6 +351,8 @@ export class ChatTranscriptBuilder {
|
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
this.#pendingUsage = settings.get("display.showTokenUsage") ? message.usage : undefined;
|
|
354
|
+
this.#pendingUsageDuration = message.duration;
|
|
355
|
+
this.#pendingUsageTtft = message.ttft;
|
|
346
356
|
}
|
|
347
357
|
|
|
348
358
|
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
|
2
3
|
import { $ } from "bun";
|
|
4
|
+
import { getDefaultPasteImageKeys } from "../../config/keybindings";
|
|
3
5
|
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
4
6
|
import {
|
|
5
7
|
CustomEditor,
|
|
@@ -124,6 +126,24 @@ describe("CustomEditor bracketed path paste", () => {
|
|
|
124
126
|
expect(imagePathCalls).toBe(0);
|
|
125
127
|
});
|
|
126
128
|
});
|
|
129
|
+
describe("CustomEditor configured paste image keys", () => {
|
|
130
|
+
it("routes Ghostty Cmd+V kitty key events through the macOS image-paste default", () => {
|
|
131
|
+
const { editor } = makeEditor();
|
|
132
|
+
const onPasteImage = vi.fn();
|
|
133
|
+
editor.onPasteImage = onPasteImage;
|
|
134
|
+
editor.setActionKeys("app.clipboard.pasteImage", getDefaultPasteImageKeys("darwin"));
|
|
135
|
+
setKittyProtocolActive(true);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
editor.handleInput("\x1b[118;9u");
|
|
139
|
+
} finally {
|
|
140
|
+
setKittyProtocolActive(false);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
expect(onPasteImage).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(editor.getText()).toBe("");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
127
147
|
|
|
128
148
|
describe("extractImagePathFromText (issue #3506)", () => {
|
|
129
149
|
it("returns the path when the text is a single image file path", () => {
|
|
@@ -113,11 +113,23 @@ function splitLeadingSpacesForWrap(line: string, width: number): { indent: strin
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** One row fed to {@link OutlinedList} or the plain list container. `highlight`
|
|
117
|
+
* causes the row (and its wrapped continuations, plus trailing padding) to be
|
|
118
|
+
* painted with the theme's `selectedBg` band — the focus cue that survives
|
|
119
|
+
* themes where `accent` fg is close to the terminal foreground. */
|
|
120
|
+
type SelectorRow = { text: string; highlight: boolean };
|
|
121
|
+
|
|
122
|
+
/** Paint `content` with the `selectedBg` background, applied AFTER any inner
|
|
123
|
+
* ANSI styling so the band spans padding as well as content. */
|
|
124
|
+
function paintSelectedRow(content: string): string {
|
|
125
|
+
return theme.bg("selectedBg", content);
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
class OutlinedList extends Container {
|
|
117
|
-
#
|
|
129
|
+
#rows: SelectorRow[] = [];
|
|
118
130
|
|
|
119
|
-
setLines(
|
|
120
|
-
this.#
|
|
131
|
+
setLines(rows: readonly SelectorRow[]): void {
|
|
132
|
+
this.#rows = rows.slice();
|
|
121
133
|
this.invalidate();
|
|
122
134
|
}
|
|
123
135
|
|
|
@@ -126,16 +138,16 @@ class OutlinedList extends Container {
|
|
|
126
138
|
const horizontal = borderColor(theme.boxRound.horizontal.repeat(Math.max(1, width)));
|
|
127
139
|
const innerWidth = Math.max(1, width - 2);
|
|
128
140
|
const content: string[] = [];
|
|
129
|
-
for (const
|
|
130
|
-
const normalized = replaceTabs(
|
|
141
|
+
for (const row of this.#rows) {
|
|
142
|
+
const normalized = replaceTabs(row.text);
|
|
131
143
|
const { indent, body } = splitLeadingSpacesForWrap(normalized, innerWidth);
|
|
132
144
|
const wrapped = wrapTextWithAnsi(body, Math.max(1, innerWidth - visibleWidth(indent)));
|
|
133
145
|
for (const wrappedBody of wrapped.length > 0 ? wrapped : [""]) {
|
|
134
146
|
const wrappedLine = `${indent}${wrappedBody}`;
|
|
135
147
|
const pad = Math.max(0, innerWidth - visibleWidth(wrappedLine));
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
148
|
+
const filled = `${wrappedLine}${padding(pad)}`;
|
|
149
|
+
const painted = row.highlight ? paintSelectedRow(filled) : filled;
|
|
150
|
+
content.push(`${borderColor(theme.boxRound.vertical)}${painted}${borderColor(theme.boxRound.vertical)}`);
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
153
|
return [horizontal, ...content, horizontal];
|
|
@@ -472,7 +484,7 @@ export class HookSelectorComponent extends Container {
|
|
|
472
484
|
}
|
|
473
485
|
|
|
474
486
|
#updateList(renderWidth = this.#lastRenderWidth): void {
|
|
475
|
-
const
|
|
487
|
+
const rows: SelectorRow[] = [];
|
|
476
488
|
const total = this.#filteredOptions.length;
|
|
477
489
|
const mdTheme = getMarkdownTheme();
|
|
478
490
|
// Compact mode kicks in exactly when the fully-expanded list (all
|
|
@@ -500,34 +512,41 @@ export class HookSelectorComponent extends Container {
|
|
|
500
512
|
const filtered = this.#filteredOptions[i];
|
|
501
513
|
if (filtered === undefined) continue;
|
|
502
514
|
const isSelected = i === this.#selectedIndex;
|
|
515
|
+
const isDisabled = this.#isDisabled(filtered.index);
|
|
503
516
|
const descMode: number | "full" = compact ? (isSelected ? selectedDescRows : 0) : "full";
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
517
|
+
// Highlight the whole option block (label + wrapped description rows)
|
|
518
|
+
// so the focus band reads as one continuous bar rather than a stripe
|
|
519
|
+
// under the label alone. Disabled rows never claim focus even if the
|
|
520
|
+
// index momentarily lands on one during initial coercion.
|
|
521
|
+
const highlight = isSelected && !isDisabled;
|
|
522
|
+
for (const text of this.#renderOptionLines(
|
|
523
|
+
filtered.option,
|
|
524
|
+
isSelected,
|
|
525
|
+
isDisabled,
|
|
526
|
+
mdTheme,
|
|
527
|
+
descMode,
|
|
528
|
+
renderWidth,
|
|
529
|
+
filtered.index,
|
|
530
|
+
)) {
|
|
531
|
+
rows.push({ text, highlight });
|
|
532
|
+
}
|
|
515
533
|
}
|
|
516
534
|
|
|
517
535
|
if (total === 0) {
|
|
518
|
-
|
|
536
|
+
rows.push({ text: theme.fg("dim", " No matching options"), highlight: false });
|
|
519
537
|
}
|
|
520
538
|
|
|
521
539
|
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus(renderWidth, mdTheme)) {
|
|
522
|
-
|
|
540
|
+
rows.push({ text: this.#renderStatusLine(total), highlight: false });
|
|
523
541
|
}
|
|
524
542
|
if (this.#outlinedList) {
|
|
525
|
-
this.#outlinedList.setLines(
|
|
543
|
+
this.#outlinedList.setLines(rows);
|
|
526
544
|
return;
|
|
527
545
|
}
|
|
528
546
|
this.#listContainer?.clear();
|
|
529
|
-
for (const
|
|
530
|
-
|
|
547
|
+
for (const row of rows) {
|
|
548
|
+
const bgFn = row.highlight ? paintSelectedRow : undefined;
|
|
549
|
+
this.#listContainer?.addChild(new Text(row.text, 1, 0, bgFn));
|
|
531
550
|
}
|
|
532
551
|
}
|
|
533
552
|
|
|
@@ -912,7 +912,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
912
912
|
}
|
|
913
913
|
#getResolvedRoleThinkingLevel(
|
|
914
914
|
role: string,
|
|
915
|
-
resolved: { explicitThinkingLevel: boolean; thinkingLevel?:
|
|
915
|
+
resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ConfiguredThinkingLevel },
|
|
916
916
|
): ConfiguredThinkingLevel {
|
|
917
917
|
if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) {
|
|
918
918
|
return resolved.thinkingLevel;
|