gsd-pi 2.73.0 → 2.73.1-dev.47ed32a
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/dist/cli.js +0 -47
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -6
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- package/dist/resources/extensions/gsd/auto.js +13 -1
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
- package/dist/resources/extensions/gsd/preferences-models.js +43 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
- package/dist/startup-model-validation.js +8 -5
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +13 -5
- package/dist/update-cmd.js +4 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.js +12 -0
- package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +4 -0
- package/packages/pi-ai/src/utils/overflow.ts +14 -1
- package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +25 -68
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +51 -26
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +116 -25
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +38 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
- package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +26 -70
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +136 -30
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -2
- package/packages/pi-tui/dist/__tests__/tui.test.js +30 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +7 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +10 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +38 -1
- package/packages/pi-tui/src/tui.ts +11 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +5 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-prompts.ts +9 -3
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- package/src/resources/extensions/gsd/auto.ts +10 -0
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/gsd-db.ts +52 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
- package/src/resources/extensions/gsd/preferences-models.ts +41 -0
- package/src/resources/extensions/gsd/preferences-types.ts +12 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +267 -0
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → WawMb3TMcYLkGJLTg_x1u}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → WawMb3TMcYLkGJLTg_x1u}/_ssgManifest.js +0 -0
|
@@ -3,45 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ThinkingLevel } from "@gsd/pi-agent-core";
|
|
6
|
-
import { type Api, type
|
|
6
|
+
import { type Api, type Model, modelsAreEqual } from "@gsd/pi-ai";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { minimatch } from "minimatch";
|
|
9
9
|
import { isValidThinkingLevel } from "../cli/args.js";
|
|
10
10
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
11
11
|
import type { ModelRegistry } from "./model-registry.js";
|
|
12
12
|
|
|
13
|
-
/** Default model IDs for each known provider */
|
|
14
|
-
const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|
15
|
-
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
|
16
|
-
anthropic: "claude-opus-4-6",
|
|
17
|
-
"anthropic-vertex": "claude-sonnet-4-6",
|
|
18
|
-
openai: "gpt-5.4",
|
|
19
|
-
"azure-openai-responses": "gpt-5.2",
|
|
20
|
-
"openai-codex": "gpt-5.4",
|
|
21
|
-
google: "gemini-2.5-pro",
|
|
22
|
-
"google-gemini-cli": "gemini-2.5-pro",
|
|
23
|
-
"google-antigravity": "gemini-3.1-pro-high",
|
|
24
|
-
"google-vertex": "gemini-3-pro-preview",
|
|
25
|
-
"github-copilot": "gpt-4o",
|
|
26
|
-
openrouter: "openai/gpt-5.1-codex",
|
|
27
|
-
"vercel-ai-gateway": "anthropic/claude-opus-4-6",
|
|
28
|
-
xai: "grok-4-fast-non-reasoning",
|
|
29
|
-
groq: "openai/gpt-oss-120b",
|
|
30
|
-
cerebras: "zai-glm-4.6",
|
|
31
|
-
zai: "glm-4.6",
|
|
32
|
-
mistral: "devstral-medium-latest",
|
|
33
|
-
minimax: "MiniMax-M2.1",
|
|
34
|
-
"minimax-cn": "MiniMax-M2.1",
|
|
35
|
-
huggingface: "moonshotai/Kimi-K2.5",
|
|
36
|
-
opencode: "claude-opus-4-6",
|
|
37
|
-
"opencode-go": "kimi-k2.5",
|
|
38
|
-
"kimi-coding": "kimi-k2-thinking",
|
|
39
|
-
"alibaba-coding-plan": "qwen3.5-plus",
|
|
40
|
-
"alibaba-dashscope": "qwen3.5-plus",
|
|
41
|
-
ollama: "llama3.1:8b",
|
|
42
|
-
"ollama-cloud": "qwen3:32b",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
13
|
export interface ScopedModel {
|
|
46
14
|
model: Model<Api>;
|
|
47
15
|
/** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */
|
|
@@ -123,10 +91,11 @@ function buildFallbackModel(provider: string, modelId: string, availableModels:
|
|
|
123
91
|
const providerModels = availableModels.filter((m) => m.provider === provider);
|
|
124
92
|
if (providerModels.length === 0) return undefined;
|
|
125
93
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
94
|
+
// Use the first available model from this provider as a template for
|
|
95
|
+
// capabilities (context window, reasoning support, etc.). The user is
|
|
96
|
+
// explicitly providing a custom model id, so we just need any shape of
|
|
97
|
+
// model from the same provider to inherit from.
|
|
98
|
+
const baseModel = providerModels[0];
|
|
130
99
|
|
|
131
100
|
return {
|
|
132
101
|
...baseModel,
|
|
@@ -503,33 +472,19 @@ export async function findInitialModel(options: {
|
|
|
503
472
|
};
|
|
504
473
|
}
|
|
505
474
|
|
|
506
|
-
// 3. Try saved default from settings
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
|
|
518
|
-
const recommended = modelRegistry.find(defaultProvider, recommendedId);
|
|
519
|
-
if (recommended) {
|
|
520
|
-
model = recommended;
|
|
521
|
-
if (defaultThinkingLevel) {
|
|
522
|
-
thinkingLevel = defaultThinkingLevel;
|
|
523
|
-
}
|
|
524
|
-
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
model = found;
|
|
528
|
-
if (defaultThinkingLevel) {
|
|
529
|
-
thinkingLevel = defaultThinkingLevel;
|
|
530
|
-
}
|
|
531
|
-
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
475
|
+
// 3. Try saved default from settings — use it exactly as configured.
|
|
476
|
+
// Whatever the user chose is what gets used; no silent substitution.
|
|
477
|
+
// Skip the saved default if its provider is not request-ready (no auth
|
|
478
|
+
// available) so we fall through to an actually-usable model instead of
|
|
479
|
+
// returning a stale selection every selector surface would display.
|
|
480
|
+
if (defaultProvider && defaultModelId && modelRegistry.isProviderRequestReady(defaultProvider)) {
|
|
481
|
+
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
|
482
|
+
if (found) {
|
|
483
|
+
model = found;
|
|
484
|
+
if (defaultThinkingLevel) {
|
|
485
|
+
thinkingLevel = defaultThinkingLevel;
|
|
532
486
|
}
|
|
487
|
+
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
533
488
|
}
|
|
534
489
|
}
|
|
535
490
|
|
|
@@ -537,16 +492,17 @@ export async function findInitialModel(options: {
|
|
|
537
492
|
const availableModels = await modelRegistry.getAvailable();
|
|
538
493
|
|
|
539
494
|
if (availableModels.length > 0) {
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
545
|
-
return { model:
|
|
495
|
+
// Prefer a model from the user's saved provider if any is still available —
|
|
496
|
+
// provider stickiness, not a hard-coded Anthropic/OpenAI preference.
|
|
497
|
+
if (defaultProvider) {
|
|
498
|
+
const sameProvider = availableModels.find((m) => m.provider === defaultProvider);
|
|
499
|
+
if (sameProvider) {
|
|
500
|
+
return { model: sameProvider, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
|
546
501
|
}
|
|
547
502
|
}
|
|
548
503
|
|
|
549
|
-
//
|
|
504
|
+
// Otherwise use the first available — registry order reflects models.json
|
|
505
|
+
// order, which the user controls.
|
|
550
506
|
return { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
|
|
551
507
|
}
|
|
552
508
|
|
|
@@ -3,8 +3,15 @@ import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-t
|
|
|
3
3
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
4
|
import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
|
|
5
5
|
|
|
6
|
+
export interface ContentRange {
|
|
7
|
+
startIndex: number;
|
|
8
|
+
endIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
|
-
* Component that renders a complete assistant message
|
|
12
|
+
* Component that renders a complete assistant message, or a sub-range of its content[].
|
|
13
|
+
* When `range` is provided, only content[startIndex..endIndex] (inclusive) is rendered.
|
|
14
|
+
* Non-text/thinking blocks within the range are silently skipped.
|
|
8
15
|
*/
|
|
9
16
|
export class AssistantMessageComponent extends Container {
|
|
10
17
|
private contentContainer: Container;
|
|
@@ -12,18 +19,26 @@ export class AssistantMessageComponent extends Container {
|
|
|
12
19
|
private markdownTheme: MarkdownTheme;
|
|
13
20
|
private lastMessage?: AssistantMessage;
|
|
14
21
|
private timestampFormat: TimestampFormat;
|
|
22
|
+
private range?: ContentRange;
|
|
23
|
+
private showMetadata: boolean;
|
|
15
24
|
|
|
16
25
|
constructor(
|
|
17
26
|
message?: AssistantMessage,
|
|
18
27
|
hideThinkingBlock = false,
|
|
19
28
|
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
|
20
29
|
timestampFormat: TimestampFormat = "date-time-iso",
|
|
30
|
+
range?: ContentRange,
|
|
21
31
|
) {
|
|
22
32
|
super();
|
|
23
33
|
|
|
24
34
|
this.hideThinkingBlock = hideThinkingBlock;
|
|
25
35
|
this.markdownTheme = markdownTheme;
|
|
26
36
|
this.timestampFormat = timestampFormat;
|
|
37
|
+
this.range = range;
|
|
38
|
+
// No range = legacy full-message rendering; show metadata by default.
|
|
39
|
+
// Ranged (interleaved) instances start with metadata hidden; chat-controller
|
|
40
|
+
// calls setShowMetadata(true) on the last segment at message_end.
|
|
41
|
+
this.showMetadata = !range;
|
|
27
42
|
|
|
28
43
|
// Container for text/thinking content
|
|
29
44
|
this.contentContainer = new Container();
|
|
@@ -34,6 +49,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
setRange(range: ContentRange | undefined): void {
|
|
53
|
+
this.range = range;
|
|
54
|
+
if (this.lastMessage) {
|
|
55
|
+
this.updateContent(this.lastMessage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setShowMetadata(show: boolean): void {
|
|
60
|
+
this.showMetadata = show;
|
|
61
|
+
if (this.lastMessage) {
|
|
62
|
+
this.updateContent(this.lastMessage);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
override invalidate(): void {
|
|
38
67
|
super.invalidate();
|
|
39
68
|
if (this.lastMessage) {
|
|
@@ -51,7 +80,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
51
80
|
// Clear content container
|
|
52
81
|
this.contentContainer.clear();
|
|
53
82
|
|
|
54
|
-
const
|
|
83
|
+
const start = this.range?.startIndex ?? 0;
|
|
84
|
+
const end = this.range?.endIndex ?? message.content.length - 1;
|
|
85
|
+
const slice = message.content.slice(start, end + 1);
|
|
86
|
+
|
|
87
|
+
const hasVisibleContent = slice.some(
|
|
55
88
|
(c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
56
89
|
);
|
|
57
90
|
|
|
@@ -59,9 +92,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
59
92
|
this.contentContainer.addChild(new Spacer(1));
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
// Render content in order
|
|
63
|
-
for (let i = 0; i <
|
|
64
|
-
const content =
|
|
95
|
+
// Render content in order; non-text/thinking blocks are silently skipped
|
|
96
|
+
for (let i = 0; i < slice.length; i++) {
|
|
97
|
+
const content = slice[i];
|
|
65
98
|
if (content.type === "text" && content.text.trim()) {
|
|
66
99
|
// Assistant text messages with no background - trim the text
|
|
67
100
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
@@ -69,7 +102,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
69
102
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
70
103
|
// Add spacing only when another visible assistant content block follows.
|
|
71
104
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
72
|
-
const hasVisibleContentAfter =
|
|
105
|
+
const hasVisibleContentAfter = slice
|
|
73
106
|
.slice(i + 1)
|
|
74
107
|
.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
75
108
|
|
|
@@ -94,30 +127,33 @@ export class AssistantMessageComponent extends Container {
|
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
// Metadata (errors, timestamp): gated on showMetadata so ranged instances stay clean
|
|
131
|
+
// until chat-controller explicitly enables it on the last segment at message_end.
|
|
132
|
+
if (this.showMetadata) {
|
|
133
|
+
// Check if aborted - show after partial content
|
|
134
|
+
// But only if there are no tool calls (tool execution components will show the error)
|
|
135
|
+
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
|
|
136
|
+
if (!hasToolCalls) {
|
|
137
|
+
if (message.stopReason === "aborted") {
|
|
138
|
+
const abortMessage =
|
|
139
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
140
|
+
? message.errorMessage
|
|
141
|
+
: "Operation aborted";
|
|
142
|
+
if (hasVisibleContent) {
|
|
143
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
144
|
+
}
|
|
145
|
+
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
146
|
+
} else if (message.stopReason === "error") {
|
|
147
|
+
const errorMsg = message.errorMessage || "Unknown error";
|
|
107
148
|
this.contentContainer.addChild(new Spacer(1));
|
|
149
|
+
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
108
150
|
}
|
|
109
|
-
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
110
|
-
} else if (message.stopReason === "error") {
|
|
111
|
-
const errorMsg = message.errorMessage || "Unknown error";
|
|
112
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
113
|
-
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
114
151
|
}
|
|
115
|
-
}
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
if (message.stopReason && message.timestamp) {
|
|
154
|
+
const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
|
|
155
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
|
|
156
|
+
}
|
|
121
157
|
}
|
|
122
158
|
}
|
|
123
159
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { findLatestPinnableText } from "./chat-controller.js";
|
|
5
|
+
|
|
6
|
+
test("findLatestPinnableText: empty content returns empty string", () => {
|
|
7
|
+
assert.equal(findLatestPinnableText([]), "");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("findLatestPinnableText: no tool calls returns empty string", () => {
|
|
11
|
+
const blocks = [
|
|
12
|
+
{ type: "text", text: "hello" },
|
|
13
|
+
{ type: "text", text: "world" },
|
|
14
|
+
];
|
|
15
|
+
assert.equal(findLatestPinnableText(blocks), "");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("findLatestPinnableText: returns text preceding a tool call", () => {
|
|
19
|
+
const blocks = [
|
|
20
|
+
{ type: "text", text: "doing the thing" },
|
|
21
|
+
{ type: "toolCall", id: "1", name: "Read" },
|
|
22
|
+
];
|
|
23
|
+
assert.equal(findLatestPinnableText(blocks), "doing the thing");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("findLatestPinnableText: ignores trailing streaming text after the last tool call (regression: pinned mirror duplicated chat-container tokens)", () => {
|
|
27
|
+
const blocks = [
|
|
28
|
+
{ type: "text", text: "first prose" },
|
|
29
|
+
{ type: "toolCall", id: "1", name: "Read" },
|
|
30
|
+
{ type: "text", text: "second prose still streaming" },
|
|
31
|
+
];
|
|
32
|
+
assert.equal(findLatestPinnableText(blocks), "first prose");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("findLatestPinnableText: with multiple tools, picks text before the most recent tool call", () => {
|
|
36
|
+
const blocks = [
|
|
37
|
+
{ type: "text", text: "first" },
|
|
38
|
+
{ type: "toolCall", id: "1", name: "Read" },
|
|
39
|
+
{ type: "text", text: "second" },
|
|
40
|
+
{ type: "toolCall", id: "2", name: "Grep" },
|
|
41
|
+
{ type: "text", text: "third streaming" },
|
|
42
|
+
];
|
|
43
|
+
assert.equal(findLatestPinnableText(blocks), "second");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("findLatestPinnableText: treats serverToolUse the same as toolCall", () => {
|
|
47
|
+
const blocks = [
|
|
48
|
+
{ type: "text", text: "before web search" },
|
|
49
|
+
{ type: "serverToolUse", id: "ws1", name: "web_search" },
|
|
50
|
+
{ type: "text", text: "answer streaming" },
|
|
51
|
+
];
|
|
52
|
+
assert.equal(findLatestPinnableText(blocks), "before web search");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("findLatestPinnableText: skips empty/whitespace-only text blocks", () => {
|
|
56
|
+
const blocks = [
|
|
57
|
+
{ type: "text", text: "real prose" },
|
|
58
|
+
{ type: "text", text: " " },
|
|
59
|
+
{ type: "text", text: "" },
|
|
60
|
+
{ type: "toolCall", id: "1", name: "Read" },
|
|
61
|
+
];
|
|
62
|
+
assert.equal(findLatestPinnableText(blocks), "real prose");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("findLatestPinnableText: thinking blocks are not pinnable", () => {
|
|
66
|
+
const blocks = [
|
|
67
|
+
{ type: "thinking", thinking: "internal" },
|
|
68
|
+
{ type: "toolCall", id: "1", name: "Read" },
|
|
69
|
+
];
|
|
70
|
+
assert.equal(findLatestPinnableText(blocks), "");
|
|
71
|
+
});
|
|
@@ -10,6 +10,17 @@ import { appKey } from "../components/keybinding-hints.js";
|
|
|
10
10
|
// Tracks the last processed content index to avoid re-scanning all blocks on every message_update
|
|
11
11
|
let lastProcessedContentIndex = 0;
|
|
12
12
|
|
|
13
|
+
// Tracks the previous content[] length so we can detect when an adapter resets
|
|
14
|
+
// the assistant content array for a new provider sub-turn within one lifecycle.
|
|
15
|
+
let lastContentLength = 0;
|
|
16
|
+
|
|
17
|
+
// --- Segment walker state (per streaming assistant turn) ---
|
|
18
|
+
type RenderedSegment =
|
|
19
|
+
| { kind: "text-run"; startIndex: number; endIndex: number; component: AssistantMessageComponent }
|
|
20
|
+
| { kind: "tool"; contentIndex: number; component: ToolExecutionComponent };
|
|
21
|
+
|
|
22
|
+
let renderedSegments: RenderedSegment[] = [];
|
|
23
|
+
|
|
13
24
|
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
14
25
|
return message.content.some(
|
|
15
26
|
(c) =>
|
|
@@ -22,6 +33,28 @@ function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
|
|
|
22
33
|
return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
// Pick the latest non-empty text block that appears strictly before the most
|
|
37
|
+
// recent tool call. Text blocks that come after the last tool call are still
|
|
38
|
+
// streaming live into the chat container, so mirroring them into the pinned
|
|
39
|
+
// "Latest Output" zone would render the same tokens twice.
|
|
40
|
+
export function findLatestPinnableText(contentBlocks: Array<any>): string {
|
|
41
|
+
let lastToolIdx = -1;
|
|
42
|
+
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
|
43
|
+
const c = contentBlocks[i];
|
|
44
|
+
if (c?.type === "toolCall" || c?.type === "serverToolUse") {
|
|
45
|
+
lastToolIdx = i;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (let i = lastToolIdx - 1; i >= 0; i--) {
|
|
50
|
+
const c = contentBlocks[i];
|
|
51
|
+
if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) {
|
|
52
|
+
return c.text.trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
// Tracks the latest assistant text for the pinned message zone
|
|
26
59
|
let lastPinnedText = "";
|
|
27
60
|
// Whether any tool execution has been added in this assistant turn (triggers pinned display)
|
|
@@ -56,8 +89,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
56
89
|
// Reset content index tracker and pinned state when a new assistant message starts
|
|
57
90
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
58
91
|
lastProcessedContentIndex = 0;
|
|
92
|
+
lastContentLength = 0;
|
|
59
93
|
lastPinnedText = "";
|
|
60
94
|
hasToolsInTurn = false;
|
|
95
|
+
renderedSegments = [];
|
|
61
96
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
62
97
|
pinnedBorder = undefined;
|
|
63
98
|
pinnedTextComponent = undefined;
|
|
@@ -77,6 +112,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
77
112
|
host.pinnedMessageContainer.clear();
|
|
78
113
|
lastPinnedText = "";
|
|
79
114
|
hasToolsInTurn = false;
|
|
115
|
+
renderedSegments = [];
|
|
116
|
+
lastContentLength = 0;
|
|
80
117
|
if (pinnedBorder) pinnedBorder.stopSpinner();
|
|
81
118
|
pinnedBorder = undefined;
|
|
82
119
|
pinnedTextComponent = undefined;
|
|
@@ -180,12 +217,22 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
180
217
|
}
|
|
181
218
|
|
|
182
219
|
const contentBlocks = host.streamingMessage.content;
|
|
183
|
-
// Some adapters reuse a single assistant
|
|
184
|
-
// spanning multiple provider turns.
|
|
185
|
-
//
|
|
186
|
-
|
|
220
|
+
// Some adapters (notably claude-code) reuse a single assistant
|
|
221
|
+
// lifecycle while internally spanning multiple provider sub-turns.
|
|
222
|
+
// When a new sub-turn starts, content[] length shrinks back to 0/1.
|
|
223
|
+
// The scan loop needs its index reset, AND the segment walker's
|
|
224
|
+
// renderedSegments map must be cleared so existing text-run
|
|
225
|
+
// components don't get overwritten in place with new sub-turn
|
|
226
|
+
// content (#4144 regression). Prior sub-turn children stay in
|
|
227
|
+
// chatContainer as frozen history; new segments append after them.
|
|
228
|
+
if (contentBlocks.length < lastContentLength) {
|
|
229
|
+
renderedSegments = [];
|
|
230
|
+
lastPinnedText = "";
|
|
231
|
+
lastProcessedContentIndex = 0;
|
|
232
|
+
} else if (lastProcessedContentIndex >= contentBlocks.length) {
|
|
187
233
|
lastProcessedContentIndex = 0;
|
|
188
234
|
}
|
|
235
|
+
lastContentLength = contentBlocks.length;
|
|
189
236
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
190
237
|
const content = contentBlocks[i];
|
|
191
238
|
if (content.type === "toolCall") {
|
|
@@ -251,24 +298,88 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
251
298
|
}
|
|
252
299
|
}
|
|
253
300
|
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
301
|
+
// Segment walker: render content blocks in stream order, append-only.
|
|
302
|
+
// Build desired segment plan from content[].
|
|
303
|
+
{
|
|
304
|
+
const blocks = host.streamingMessage.content;
|
|
305
|
+
type DesiredSegment =
|
|
306
|
+
| { kind: "text-run"; startIndex: number; endIndex: number }
|
|
307
|
+
| { kind: "tool"; contentIndex: number; toolId: string };
|
|
308
|
+
const desired: DesiredSegment[] = [];
|
|
309
|
+
let runStart = -1;
|
|
310
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
311
|
+
const b = blocks[i];
|
|
312
|
+
const isText = b.type === "text" || b.type === "thinking";
|
|
313
|
+
const isTool = b.type === "toolCall" || b.type === "serverToolUse";
|
|
314
|
+
if (isText) {
|
|
315
|
+
if (runStart === -1) runStart = i;
|
|
316
|
+
} else {
|
|
317
|
+
if (runStart !== -1) {
|
|
318
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: i - 1 });
|
|
319
|
+
runStart = -1;
|
|
320
|
+
}
|
|
321
|
+
if (isTool) {
|
|
322
|
+
desired.push({ kind: "tool", contentIndex: i, toolId: b.id });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (runStart !== -1) {
|
|
327
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: blocks.length - 1 });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Append any newly needed segments (never reorder existing ones).
|
|
331
|
+
for (const seg of desired) {
|
|
332
|
+
if (seg.kind === "tool") {
|
|
333
|
+
// Tool segments are already handled above via pendingTools; just
|
|
334
|
+
// register them in renderedSegments if not yet tracked.
|
|
335
|
+
const existing = renderedSegments.find(
|
|
336
|
+
(s) => s.kind === "tool" && s.contentIndex === seg.contentIndex,
|
|
337
|
+
);
|
|
338
|
+
if (!existing) {
|
|
339
|
+
const comp = host.pendingTools.get(seg.toolId);
|
|
340
|
+
if (comp) {
|
|
341
|
+
renderedSegments.push({ kind: "tool", contentIndex: seg.contentIndex, component: comp });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// text-run segment
|
|
346
|
+
const existing = renderedSegments.find(
|
|
347
|
+
(s) => s.kind === "text-run" && s.startIndex === seg.startIndex,
|
|
348
|
+
);
|
|
349
|
+
if (!existing) {
|
|
350
|
+
const comp = new AssistantMessageComponent(
|
|
351
|
+
undefined,
|
|
352
|
+
host.hideThinkingBlock,
|
|
353
|
+
host.getMarkdownThemeWithSettings(),
|
|
354
|
+
host.settingsManager.getTimestampFormat(),
|
|
355
|
+
{ startIndex: seg.startIndex, endIndex: seg.endIndex },
|
|
356
|
+
);
|
|
357
|
+
host.chatContainer.addChild(comp);
|
|
358
|
+
renderedSegments.push({ kind: "text-run", startIndex: seg.startIndex, endIndex: seg.endIndex, component: comp });
|
|
359
|
+
host.streamingComponent = comp;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Update all trailing text-run segments with the latest message so
|
|
365
|
+
// streaming text grows in place.
|
|
366
|
+
for (const seg of renderedSegments) {
|
|
367
|
+
if (seg.kind === "text-run") {
|
|
368
|
+
// Find corresponding desired segment to get current endIndex
|
|
369
|
+
const d = desired.find((ds) => ds.kind === "text-run" && ds.startIndex === seg.startIndex);
|
|
370
|
+
if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) {
|
|
371
|
+
seg.endIndex = d.endIndex;
|
|
372
|
+
seg.component.setRange({ startIndex: seg.startIndex, endIndex: seg.endIndex });
|
|
373
|
+
}
|
|
374
|
+
seg.component.updateContent(host.streamingMessage);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Keep streamingComponent pointing at the last text-run for message_end compatibility.
|
|
379
|
+
const lastTextSeg = [...renderedSegments].reverse().find((s) => s.kind === "text-run");
|
|
380
|
+
if (lastTextSeg && lastTextSeg.kind === "text-run") {
|
|
381
|
+
host.streamingComponent = lastTextSeg.component;
|
|
270
382
|
}
|
|
271
|
-
host.streamingComponent.updateContent(host.streamingMessage);
|
|
272
383
|
}
|
|
273
384
|
|
|
274
385
|
// Update index: fully processed blocks won't need re-scanning.
|
|
@@ -286,15 +397,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
286
397
|
if (hasTools) hasToolsInTurn = true;
|
|
287
398
|
|
|
288
399
|
if (hasToolsInTurn) {
|
|
289
|
-
|
|
290
|
-
let latestText = "";
|
|
291
|
-
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
|
292
|
-
const c = contentBlocks[i] as any;
|
|
293
|
-
if (c.type === "text" && c.text?.trim()) {
|
|
294
|
-
latestText = c.text.trim();
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
400
|
+
const latestText = findLatestPinnableText(contentBlocks);
|
|
298
401
|
|
|
299
402
|
if (latestText && latestText !== lastPinnedText) {
|
|
300
403
|
lastPinnedText = latestText;
|
|
@@ -362,6 +465,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
362
465
|
host.chatContainer.addChild(host.streamingComponent);
|
|
363
466
|
}
|
|
364
467
|
if (host.streamingComponent) {
|
|
468
|
+
host.streamingComponent.setShowMetadata(true);
|
|
365
469
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
366
470
|
}
|
|
367
471
|
|
|
@@ -385,6 +489,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
385
489
|
}
|
|
386
490
|
host.streamingComponent = undefined;
|
|
387
491
|
host.streamingMessage = undefined;
|
|
492
|
+
renderedSegments = [];
|
|
493
|
+
lastContentLength = 0;
|
|
388
494
|
// Clear pinned output once the message is finalized in the chat
|
|
389
495
|
// container — prevents duplicate display when the agent continues
|
|
390
496
|
// (e.g. form elicitation) after the assistant message ends.
|
|
@@ -1402,7 +1402,9 @@ export class InteractiveMode {
|
|
|
1402
1402
|
|
|
1403
1403
|
// widgetContainerAbove: spacer collapses when pinned content is visible
|
|
1404
1404
|
// so there's no extra blank line between pinned output and the editor border.
|
|
1405
|
-
|
|
1405
|
+
// Use detachChildren() (not clear()) — the extensionWidgetsAbove map owns
|
|
1406
|
+
// disposal; clear() would dispose every mounted widget on every re-render.
|
|
1407
|
+
this.widgetContainerAbove.detachChildren();
|
|
1406
1408
|
const pinned = this.pinnedMessageContainer;
|
|
1407
1409
|
this.widgetContainerAbove.addChild({
|
|
1408
1410
|
render: () => pinned.children.length > 0 ? [] : [""],
|
|
@@ -1422,7 +1424,9 @@ export class InteractiveMode {
|
|
|
1422
1424
|
spacerWhenEmpty: boolean,
|
|
1423
1425
|
leadingSpacer: boolean,
|
|
1424
1426
|
): void {
|
|
1425
|
-
|
|
1427
|
+
// Detach without disposing — the widgets map owns lifecycle; disposing
|
|
1428
|
+
// here would kill refresh timers and subscriptions on every re-render.
|
|
1429
|
+
container.detachChildren();
|
|
1426
1430
|
|
|
1427
1431
|
if (widgets.size === 0) {
|
|
1428
1432
|
if (spacerWhenEmpty) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { TUI } from "../tui.js";
|
|
3
|
+
import { Container, TUI } from "../tui.js";
|
|
4
4
|
function makeTerminal() {
|
|
5
5
|
return {
|
|
6
6
|
isTTY: true,
|
|
@@ -40,4 +40,33 @@ describe("TUI", () => {
|
|
|
40
40
|
assert.equal(anyTui.inputBuffer, "");
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
describe("Container", () => {
|
|
44
|
+
function makeDisposableChild(counter) {
|
|
45
|
+
return {
|
|
46
|
+
render: () => [],
|
|
47
|
+
invalidate() { },
|
|
48
|
+
dispose() {
|
|
49
|
+
counter.disposed++;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
it("detachChildren() removes children without disposing them", () => {
|
|
54
|
+
const c = new Container();
|
|
55
|
+
const counter = { disposed: 0 };
|
|
56
|
+
c.addChild(makeDisposableChild(counter));
|
|
57
|
+
c.addChild(makeDisposableChild(counter));
|
|
58
|
+
c.detachChildren();
|
|
59
|
+
assert.equal(c.children.length, 0);
|
|
60
|
+
assert.equal(counter.disposed, 0);
|
|
61
|
+
});
|
|
62
|
+
it("clear() still disposes children (regression guard for detach/dispose split)", () => {
|
|
63
|
+
const c = new Container();
|
|
64
|
+
const counter = { disposed: 0 };
|
|
65
|
+
c.addChild(makeDisposableChild(counter));
|
|
66
|
+
c.addChild(makeDisposableChild(counter));
|
|
67
|
+
c.clear();
|
|
68
|
+
assert.equal(c.children.length, 0);
|
|
69
|
+
assert.equal(counter.disposed, 2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
43
72
|
//# sourceMappingURL=tui.test.js.map
|