gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.06e4302
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-web-branch.d.ts +4 -3
- package/dist/cli-web-branch.js +10 -7
- package/dist/cli.js +99 -206
- package/dist/logo.d.ts +1 -1
- package/dist/logo.js +1 -1
- package/dist/onboarding.js +59 -53
- package/dist/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +68 -4
- package/dist/resources/extensions/gsd/auto/phases.js +15 -9
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-verification.js +88 -3
- package/dist/resources/extensions/gsd/auto.js +34 -9
- 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/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- 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/notification-widget.js +2 -2
- 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/resources/extensions/gsd/state.js +61 -14
- 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 +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- 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/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/package.json +1 -2
- 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/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/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 +61 -28
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +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 +94 -16
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- 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 +11 -3
- 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/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/modes/interactive/components/assistant-message.ts +74 -32
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +113 -21
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +107 -5
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +111 -2
- package/src/resources/extensions/gsd/auto/phases.ts +22 -9
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
- package/src/resources/extensions/gsd/auto-verification.ts +98 -3
- package/src/resources/extensions/gsd/auto.ts +36 -14
- 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/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- 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/notification-widget.ts +2 -2
- 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/state.ts +71 -15
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- 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/integration/state-machine-edge-cases.test.ts +4 -2
- 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/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { isContextOverflow } from "../overflow.js";
|
|
4
|
+
function makeAssistantMessage(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
role: "assistant",
|
|
7
|
+
content: [],
|
|
8
|
+
api: "anthropic-messages",
|
|
9
|
+
provider: "anthropic",
|
|
10
|
+
model: "claude-sonnet-4-6",
|
|
11
|
+
usage: {
|
|
12
|
+
input: 0,
|
|
13
|
+
output: 0,
|
|
14
|
+
cacheRead: 0,
|
|
15
|
+
cacheWrite: 0,
|
|
16
|
+
totalTokens: 0,
|
|
17
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
18
|
+
},
|
|
19
|
+
stopReason: "error",
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
describe("isContextOverflow", () => {
|
|
25
|
+
test("detects overflow from provider errorMessage", () => {
|
|
26
|
+
const message = makeAssistantMessage({
|
|
27
|
+
errorMessage: "prompt is too long: 213462 tokens > 200000 maximum",
|
|
28
|
+
});
|
|
29
|
+
assert.equal(isContextOverflow(message, 200000), true);
|
|
30
|
+
});
|
|
31
|
+
test("detects claude-code overflow when text contains the error but errorMessage is generic (#3925)", () => {
|
|
32
|
+
const message = makeAssistantMessage({
|
|
33
|
+
provider: "claude-code",
|
|
34
|
+
api: "anthropic-messages",
|
|
35
|
+
model: "claude-sonnet-4-6",
|
|
36
|
+
errorMessage: "success",
|
|
37
|
+
content: [{ type: "text", text: "Prompt is too long" }],
|
|
38
|
+
});
|
|
39
|
+
assert.equal(isContextOverflow(message, 200000), true);
|
|
40
|
+
});
|
|
41
|
+
test("does not treat normal non-error text as overflow", () => {
|
|
42
|
+
const message = makeAssistantMessage({
|
|
43
|
+
stopReason: "stop",
|
|
44
|
+
errorMessage: undefined,
|
|
45
|
+
content: [{ type: "text", text: "Prompt is too long" }],
|
|
46
|
+
});
|
|
47
|
+
assert.equal(isContextOverflow(message, 200000), false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=overflow.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overflow.test.js","sourceRoot":"","sources":["../../../src/utils/tests/overflow.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGnD,SAAS,oBAAoB,CAAC,YAAuC,EAAE;IACtE,OAAO;QACN,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,EAAE;QACX,GAAG,EAAE,oBAAoB;QACzB,QAAQ,EAAE,WAAW;QACrB,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE;YACN,KAAK,EAAE,CAAC;YACR,MAAM,EAAE,CAAC;YACT,SAAS,EAAE,CAAC;YACZ,UAAU,EAAE,CAAC;YACb,WAAW,EAAE,CAAC;YACd,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;SACpE;QACD,UAAU,EAAE,OAAO;QACnB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,GAAG,SAAS;KACZ,CAAC;AACH,CAAC;AAED,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAClC,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACxD,MAAM,OAAO,GAAG,oBAAoB,CAAC;YACpC,YAAY,EAAE,oDAAoD;SAClE,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+FAA+F,EAAE,GAAG,EAAE;QAC1G,MAAM,OAAO,GAAG,oBAAoB,CAAC;YACpC,QAAQ,EAAE,aAAa;YACvB,GAAG,EAAE,oBAAoB;YACzB,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,SAAS;YACvB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;SACvD,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC7D,MAAM,OAAO,GAAG,oBAAoB,CAAC;YACpC,UAAU,EAAE,MAAM;YAClB,YAAY,EAAE,SAAS;YACvB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;SACvD,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import { describe, test } from \"node:test\";\nimport assert from \"node:assert/strict\";\n\nimport { isContextOverflow } from \"../overflow.js\";\nimport type { AssistantMessage } from \"../../types.js\";\n\nfunction makeAssistantMessage(overrides: Partial<AssistantMessage> = {}): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [],\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"anthropic\",\n\t\tmodel: \"claude-sonnet-4-6\",\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"error\",\n\t\ttimestamp: Date.now(),\n\t\t...overrides,\n\t};\n}\n\ndescribe(\"isContextOverflow\", () => {\n\ttest(\"detects overflow from provider errorMessage\", () => {\n\t\tconst message = makeAssistantMessage({\n\t\t\terrorMessage: \"prompt is too long: 213462 tokens > 200000 maximum\",\n\t\t});\n\n\t\tassert.equal(isContextOverflow(message, 200000), true);\n\t});\n\n\ttest(\"detects claude-code overflow when text contains the error but errorMessage is generic (#3925)\", () => {\n\t\tconst message = makeAssistantMessage({\n\t\t\tprovider: \"claude-code\",\n\t\t\tapi: \"anthropic-messages\",\n\t\t\tmodel: \"claude-sonnet-4-6\",\n\t\t\terrorMessage: \"success\",\n\t\t\tcontent: [{ type: \"text\", text: \"Prompt is too long\" }],\n\t\t});\n\n\t\tassert.equal(isContextOverflow(message, 200000), true);\n\t});\n\n\ttest(\"does not treat normal non-error text as overflow\", () => {\n\t\tconst message = makeAssistantMessage({\n\t\t\tstopReason: \"stop\",\n\t\t\terrorMessage: undefined,\n\t\t\tcontent: [{ type: \"text\", text: \"Prompt is too long\" }],\n\t\t});\n\n\t\tassert.equal(isContextOverflow(message, 200000), false);\n\t});\n});\n"]}
|
|
@@ -5,6 +5,10 @@ export * from "./api-registry.js";
|
|
|
5
5
|
export * from "./env-api-keys.js";
|
|
6
6
|
export * from "./models.js";
|
|
7
7
|
export * from "./providers/anthropic.js";
|
|
8
|
+
export {
|
|
9
|
+
mapThinkingLevelToEffort,
|
|
10
|
+
supportsAdaptiveThinking,
|
|
11
|
+
} from "./providers/anthropic-shared.js";
|
|
8
12
|
export * from "./providers/azure-openai-responses.js";
|
|
9
13
|
export * from "./providers/google.js";
|
|
10
14
|
export * from "./providers/google-gemini-cli.js";
|
|
@@ -104,6 +104,19 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Some providers surface overflow as assistant text while putting a generic
|
|
108
|
+
// classifier value in errorMessage (e.g. claude-code: errorMessage="success",
|
|
109
|
+
// text="Prompt is too long"). Check rendered text as a fallback.
|
|
110
|
+
if (message.stopReason === "error") {
|
|
111
|
+
const assistantText = message.content
|
|
112
|
+
.filter((block) => block.type === "text")
|
|
113
|
+
.map((block) => block.text)
|
|
114
|
+
.join("\n");
|
|
115
|
+
if (assistantText && OVERFLOW_PATTERNS.some((p) => p.test(assistantText))) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
// Case 2: Silent overflow (z.ai style) - successful but usage exceeds context
|
|
108
121
|
if (contextWindow && message.stopReason === "stop") {
|
|
109
122
|
const inputTokens = message.usage.input + message.usage.cacheRead;
|
|
@@ -113,4 +126,4 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
|
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
return false;
|
|
116
|
-
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { isContextOverflow } from "../overflow.js";
|
|
5
|
+
import type { AssistantMessage } from "../../types.js";
|
|
6
|
+
|
|
7
|
+
function makeAssistantMessage(overrides: Partial<AssistantMessage> = {}): AssistantMessage {
|
|
8
|
+
return {
|
|
9
|
+
role: "assistant",
|
|
10
|
+
content: [],
|
|
11
|
+
api: "anthropic-messages",
|
|
12
|
+
provider: "anthropic",
|
|
13
|
+
model: "claude-sonnet-4-6",
|
|
14
|
+
usage: {
|
|
15
|
+
input: 0,
|
|
16
|
+
output: 0,
|
|
17
|
+
cacheRead: 0,
|
|
18
|
+
cacheWrite: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
21
|
+
},
|
|
22
|
+
stopReason: "error",
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("isContextOverflow", () => {
|
|
29
|
+
test("detects overflow from provider errorMessage", () => {
|
|
30
|
+
const message = makeAssistantMessage({
|
|
31
|
+
errorMessage: "prompt is too long: 213462 tokens > 200000 maximum",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(isContextOverflow(message, 200000), true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("detects claude-code overflow when text contains the error but errorMessage is generic (#3925)", () => {
|
|
38
|
+
const message = makeAssistantMessage({
|
|
39
|
+
provider: "claude-code",
|
|
40
|
+
api: "anthropic-messages",
|
|
41
|
+
model: "claude-sonnet-4-6",
|
|
42
|
+
errorMessage: "success",
|
|
43
|
+
content: [{ type: "text", text: "Prompt is too long" }],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.equal(isContextOverflow(message, 200000), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("does not treat normal non-error text as overflow", () => {
|
|
50
|
+
const message = makeAssistantMessage({
|
|
51
|
+
stopReason: "stop",
|
|
52
|
+
errorMessage: undefined,
|
|
53
|
+
content: [{ type: "text", text: "Prompt is too long" }],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(isContextOverflow(message, 200000), false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -88,7 +88,7 @@ function createHost() {
|
|
|
88
88
|
};
|
|
89
89
|
return host;
|
|
90
90
|
}
|
|
91
|
-
test("chat-controller
|
|
91
|
+
test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => {
|
|
92
92
|
// ToolExecutionComponent uses the global theme singleton.
|
|
93
93
|
// Install a minimal no-op theme implementation for this unit test.
|
|
94
94
|
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
@@ -107,7 +107,6 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
107
107
|
arguments: { cmd: "echo hi" },
|
|
108
108
|
};
|
|
109
109
|
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) });
|
|
110
|
-
assert.equal(host.streamingComponent, undefined, "assistant component should be deferred at message_start");
|
|
111
110
|
assert.equal(host.chatContainer.children.length, 0, "nothing should render before content arrives");
|
|
112
111
|
await handleAgentEvent(host, {
|
|
113
112
|
type: "message_update",
|
|
@@ -126,10 +125,9 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
126
125
|
partial: makeAssistant([toolCall]),
|
|
127
126
|
},
|
|
128
127
|
});
|
|
129
|
-
|
|
128
|
+
// content[0] = toolCall → ToolExecutionComponent renders first
|
|
130
129
|
assert.equal(host.chatContainer.children.length, 1, "tool execution block should render immediately");
|
|
131
130
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
132
|
-
// Re-assert required host method before the text-bearing update path.
|
|
133
131
|
host.getMarkdownThemeWithSettings = () => ({});
|
|
134
132
|
await handleAgentEvent(host, {
|
|
135
133
|
type: "message_update",
|
|
@@ -141,11 +139,12 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
141
139
|
partial: makeAssistant([toolCall, { type: "text", text: "done" }]),
|
|
142
140
|
},
|
|
143
141
|
});
|
|
144
|
-
|
|
142
|
+
// content[0]=toolCall, content[1]=text → order: tool, then text
|
|
143
|
+
assert.equal(host.chatContainer.children.length, 2, "text run should render after tool in content[] order");
|
|
145
144
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
146
145
|
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
147
146
|
});
|
|
148
|
-
test("chat-controller
|
|
147
|
+
test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => {
|
|
149
148
|
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
150
149
|
fg: (_key, text) => text,
|
|
151
150
|
bg: (_key, text) => text,
|
|
@@ -171,7 +170,7 @@ test("chat-controller keeps serverToolUse output ahead of assistant text when ex
|
|
|
171
170
|
partial: makeAssistant([serverToolUse]),
|
|
172
171
|
},
|
|
173
172
|
});
|
|
174
|
-
|
|
173
|
+
// content[0] = serverToolUse → ToolExecutionComponent renders first
|
|
175
174
|
assert.equal(host.chatContainer.children.length, 1, "server tool block should render immediately");
|
|
176
175
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
177
176
|
host.getMarkdownThemeWithSettings = () => ({});
|
|
@@ -195,7 +194,8 @@ test("chat-controller keeps serverToolUse output ahead of assistant text when ex
|
|
|
195
194
|
partial: resultMessage,
|
|
196
195
|
},
|
|
197
196
|
});
|
|
198
|
-
|
|
197
|
+
// content[0]=serverToolUse, content[1]=text → order: tool, then text
|
|
198
|
+
assert.equal(host.chatContainer.children.length, 2, "text run should render after server tool in content[] order");
|
|
199
199
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
200
200
|
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
201
201
|
});
|
|
@@ -385,4 +385,309 @@ test("chat-controller does not pin when there are no tool calls", async () => {
|
|
|
385
385
|
});
|
|
386
386
|
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should stay empty without tool calls");
|
|
387
387
|
});
|
|
388
|
+
// Regression test for issue #4144: interleaved text/tool content must render in content[] index order.
|
|
389
|
+
// Stream: [text "A", toolCall T1, text "B", toolCall T2, text "C"]
|
|
390
|
+
// Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C)
|
|
391
|
+
// Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end.
|
|
392
|
+
test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => {
|
|
393
|
+
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
394
|
+
fg: (_key, text) => text,
|
|
395
|
+
bg: (_key, text) => text,
|
|
396
|
+
bold: (text) => text,
|
|
397
|
+
italic: (text) => text,
|
|
398
|
+
truncate: (text) => text,
|
|
399
|
+
};
|
|
400
|
+
const host = createHost();
|
|
401
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
402
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
403
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
404
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) });
|
|
405
|
+
// Stream text "A" at index 0
|
|
406
|
+
await handleAgentEvent(host, {
|
|
407
|
+
type: "message_update",
|
|
408
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
409
|
+
assistantMessageEvent: {
|
|
410
|
+
type: "text_delta",
|
|
411
|
+
contentIndex: 0,
|
|
412
|
+
delta: "A",
|
|
413
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
// Stream toolCall T1 at index 1
|
|
417
|
+
await handleAgentEvent(host, {
|
|
418
|
+
type: "message_update",
|
|
419
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
420
|
+
assistantMessageEvent: {
|
|
421
|
+
type: "toolcall_end",
|
|
422
|
+
contentIndex: 1,
|
|
423
|
+
toolCall: {
|
|
424
|
+
...t1,
|
|
425
|
+
externalResult: { content: [{ type: "text", text: "result1" }], details: {}, isError: false },
|
|
426
|
+
},
|
|
427
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
// Stream text "B" at index 2
|
|
431
|
+
await handleAgentEvent(host, {
|
|
432
|
+
type: "message_update",
|
|
433
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
434
|
+
assistantMessageEvent: {
|
|
435
|
+
type: "text_delta",
|
|
436
|
+
contentIndex: 2,
|
|
437
|
+
delta: "B",
|
|
438
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
// Stream toolCall T2 at index 3
|
|
442
|
+
await handleAgentEvent(host, {
|
|
443
|
+
type: "message_update",
|
|
444
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2]),
|
|
445
|
+
assistantMessageEvent: {
|
|
446
|
+
type: "toolcall_end",
|
|
447
|
+
contentIndex: 3,
|
|
448
|
+
toolCall: {
|
|
449
|
+
...t2,
|
|
450
|
+
externalResult: { content: [{ type: "text", text: "result2" }], details: {}, isError: false },
|
|
451
|
+
},
|
|
452
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2]),
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
// Stream text "C" at index 4
|
|
456
|
+
const finalContent = [
|
|
457
|
+
{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2, { type: "text", text: "C" },
|
|
458
|
+
];
|
|
459
|
+
await handleAgentEvent(host, {
|
|
460
|
+
type: "message_update",
|
|
461
|
+
message: makeAssistant(finalContent),
|
|
462
|
+
assistantMessageEvent: {
|
|
463
|
+
type: "text_delta",
|
|
464
|
+
contentIndex: 4,
|
|
465
|
+
delta: "C",
|
|
466
|
+
partial: makeAssistant(finalContent),
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
// Finalize — exercises the message_end path where a buggy setRange(undefined) would cause duplication
|
|
470
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant(finalContent) });
|
|
471
|
+
// Assert interleaved order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C)
|
|
472
|
+
assert.equal(host.chatContainer.children.length, 5, "should have 5 children in interleaved order");
|
|
473
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent", "index 0: text run A");
|
|
474
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent", "index 1: tool T1");
|
|
475
|
+
assert.equal(host.chatContainer.children[2]?.constructor?.name, "AssistantMessageComponent", "index 2: text run B");
|
|
476
|
+
assert.equal(host.chatContainer.children[3]?.constructor?.name, "ToolExecutionComponent", "index 3: tool T2");
|
|
477
|
+
assert.equal(host.chatContainer.children[4]?.constructor?.name, "AssistantMessageComponent", "index 4: text run C");
|
|
478
|
+
// Helper: collect the text of all Markdown children inside an AssistantMessageComponent.
|
|
479
|
+
// Structure: AssistantMessageComponent (Container) -> contentContainer (children[0]) -> Markdown nodes.
|
|
480
|
+
function getRenderedTexts(comp) {
|
|
481
|
+
const contentContainer = comp.children?.[0];
|
|
482
|
+
if (!contentContainer)
|
|
483
|
+
return [];
|
|
484
|
+
return (contentContainer.children ?? [])
|
|
485
|
+
.filter((c) => c.constructor?.name === "Markdown")
|
|
486
|
+
.map((c) => c.text);
|
|
487
|
+
}
|
|
488
|
+
// Each text-run component must contain only its own text — no cross-contamination after message_end
|
|
489
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[0]), ["A"], "text run A must contain only 'A'");
|
|
490
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[2]), ["B"], "text run B must contain only 'B'");
|
|
491
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[4]), ["C"], "text run C must contain only 'C'");
|
|
492
|
+
});
|
|
493
|
+
test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => {
|
|
494
|
+
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
495
|
+
fg: (_key, text) => text,
|
|
496
|
+
bg: (_key, text) => text,
|
|
497
|
+
bold: (text) => text,
|
|
498
|
+
italic: (text) => text,
|
|
499
|
+
truncate: (text) => text,
|
|
500
|
+
};
|
|
501
|
+
const host = createHost();
|
|
502
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
503
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
504
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) });
|
|
505
|
+
// Step 1: text "A" at index 0
|
|
506
|
+
await handleAgentEvent(host, {
|
|
507
|
+
type: "message_update",
|
|
508
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
509
|
+
assistantMessageEvent: {
|
|
510
|
+
type: "text_delta",
|
|
511
|
+
contentIndex: 0,
|
|
512
|
+
delta: "A",
|
|
513
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
// Step 2: toolCall at index 1
|
|
517
|
+
await handleAgentEvent(host, {
|
|
518
|
+
type: "message_update",
|
|
519
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
520
|
+
assistantMessageEvent: {
|
|
521
|
+
type: "toolcall_end",
|
|
522
|
+
contentIndex: 1,
|
|
523
|
+
toolCall: {
|
|
524
|
+
...t1,
|
|
525
|
+
externalResult: { content: [{ type: "text", text: "result1" }], details: {}, isError: false },
|
|
526
|
+
},
|
|
527
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
// Step 3: text "B" at index 2
|
|
531
|
+
const finalContent = [{ type: "text", text: "A" }, t1, { type: "text", text: "B" }];
|
|
532
|
+
await handleAgentEvent(host, {
|
|
533
|
+
type: "message_update",
|
|
534
|
+
message: makeAssistant(finalContent),
|
|
535
|
+
assistantMessageEvent: {
|
|
536
|
+
type: "text_delta",
|
|
537
|
+
contentIndex: 2,
|
|
538
|
+
delta: "B",
|
|
539
|
+
partial: makeAssistant(finalContent),
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
assert.equal(host.chatContainer.children.length, 3);
|
|
543
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
544
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent");
|
|
545
|
+
assert.equal(host.chatContainer.children[2]?.constructor?.name, "AssistantMessageComponent");
|
|
546
|
+
const firstText = host.chatContainer.children[0];
|
|
547
|
+
const secondText = host.chatContainer.children[2];
|
|
548
|
+
assert.notEqual(firstText, secondText, "text-before-tool and text-after-tool must be separate component instances");
|
|
549
|
+
assert.deepEqual(firstText.range, { startIndex: 0, endIndex: 0 }, "first text-run covers only content[0]");
|
|
550
|
+
assert.deepEqual(secondText.range, { startIndex: 2, endIndex: 2 }, "second text-run covers only content[2]");
|
|
551
|
+
// Finalize — regression guard: range must NOT be cleared on message_end (would cause duplication)
|
|
552
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant(finalContent) });
|
|
553
|
+
assert.deepEqual(secondText.range, { startIndex: 2, endIndex: 2 }, "range must not be cleared on message_end (would cause duplication)");
|
|
554
|
+
});
|
|
555
|
+
// Regression for the claude-code sub-turn bug that followed #4144:
|
|
556
|
+
// an adapter can reset content[] back to 0/1 mid-lifecycle when a new provider
|
|
557
|
+
// sub-turn begins. The segment walker must NOT update prior-sub-turn text-run
|
|
558
|
+
// components in place (which would destroy earlier history) and must NOT reuse
|
|
559
|
+
// stale tool registrations for a new tool at the same contentIndex. Prior
|
|
560
|
+
// sub-turn children must stay frozen; new sub-turn segments must append after
|
|
561
|
+
// them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn.
|
|
562
|
+
test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => {
|
|
563
|
+
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
564
|
+
fg: (_key, text) => text,
|
|
565
|
+
bg: (_key, text) => text,
|
|
566
|
+
bold: (text) => text,
|
|
567
|
+
italic: (text) => text,
|
|
568
|
+
truncate: (text) => text,
|
|
569
|
+
};
|
|
570
|
+
const host = createHost();
|
|
571
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
572
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
573
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
574
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) });
|
|
575
|
+
// Sub-turn 1: grow to [A, T1, B]
|
|
576
|
+
await handleAgentEvent(host, {
|
|
577
|
+
type: "message_update",
|
|
578
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
579
|
+
assistantMessageEvent: {
|
|
580
|
+
type: "text_delta", contentIndex: 0, delta: "A",
|
|
581
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
await handleAgentEvent(host, {
|
|
585
|
+
type: "message_update",
|
|
586
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
587
|
+
assistantMessageEvent: {
|
|
588
|
+
type: "toolcall_end", contentIndex: 1,
|
|
589
|
+
toolCall: { ...t1, externalResult: { content: [{ type: "text", text: "r1" }], details: {}, isError: false } },
|
|
590
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
await handleAgentEvent(host, {
|
|
594
|
+
type: "message_update",
|
|
595
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
596
|
+
assistantMessageEvent: {
|
|
597
|
+
type: "text_delta", contentIndex: 2, delta: "B",
|
|
598
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
assert.equal(host.chatContainer.children.length, 3, "sub-turn 1 renders 3 children");
|
|
602
|
+
const priorA = host.chatContainer.children[0];
|
|
603
|
+
const priorT1 = host.chatContainer.children[1];
|
|
604
|
+
const priorB = host.chatContainer.children[2];
|
|
605
|
+
// Sub-turn boundary: adapter resets content[] to [C]
|
|
606
|
+
await handleAgentEvent(host, {
|
|
607
|
+
type: "message_update",
|
|
608
|
+
message: makeAssistant([{ type: "text", text: "C" }]),
|
|
609
|
+
assistantMessageEvent: {
|
|
610
|
+
type: "text_delta", contentIndex: 0, delta: "C",
|
|
611
|
+
partial: makeAssistant([{ type: "text", text: "C" }]),
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
// Prior 3 children must still exist in DOM — and a NEW text-run for "C" appended after them.
|
|
615
|
+
assert.equal(host.chatContainer.children.length, 4, "shrink must append new segment, not replace prior history");
|
|
616
|
+
assert.equal(host.chatContainer.children[0], priorA, "prior A component stays at index 0");
|
|
617
|
+
assert.equal(host.chatContainer.children[1], priorT1, "prior T1 component stays at index 1");
|
|
618
|
+
assert.equal(host.chatContainer.children[2], priorB, "prior B component stays at index 2");
|
|
619
|
+
assert.notEqual(host.chatContainer.children[3], priorA, "new C text-run must be a different component from prior A");
|
|
620
|
+
assert.equal(host.chatContainer.children[3]?.constructor?.name, "AssistantMessageComponent");
|
|
621
|
+
// Prior A component must still render "A", not be overwritten with "C".
|
|
622
|
+
function getRenderedTexts(comp) {
|
|
623
|
+
const contentContainer = comp.children?.[0];
|
|
624
|
+
if (!contentContainer)
|
|
625
|
+
return [];
|
|
626
|
+
return (contentContainer.children ?? [])
|
|
627
|
+
.filter((c) => c.constructor?.name === "Markdown")
|
|
628
|
+
.map((c) => c.text);
|
|
629
|
+
}
|
|
630
|
+
assert.deepEqual(getRenderedTexts(priorA), ["A"], "prior A text-run must still contain 'A' after shrink");
|
|
631
|
+
assert.deepEqual(getRenderedTexts(priorB), ["B"], "prior B text-run must still contain 'B' after shrink");
|
|
632
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[3]), ["C"], "new text-run must contain only 'C'");
|
|
633
|
+
// Sub-turn 2 grows with a new tool T2 at contentIndex=1.
|
|
634
|
+
await handleAgentEvent(host, {
|
|
635
|
+
type: "message_update",
|
|
636
|
+
message: makeAssistant([{ type: "text", text: "C" }, t2]),
|
|
637
|
+
assistantMessageEvent: {
|
|
638
|
+
type: "toolcall_end", contentIndex: 1,
|
|
639
|
+
toolCall: { ...t2, externalResult: { content: [{ type: "text", text: "r2" }], details: {}, isError: false } },
|
|
640
|
+
partial: makeAssistant([{ type: "text", text: "C" }, t2]),
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
// T2 must be appended after the new C text-run, not conflated with the stale T1 registration.
|
|
644
|
+
assert.equal(host.chatContainer.children.length, 5, "new tool appends after new text-run");
|
|
645
|
+
assert.equal(host.chatContainer.children[4]?.constructor?.name, "ToolExecutionComponent");
|
|
646
|
+
assert.notEqual(host.chatContainer.children[4], priorT1, "new T2 must be a different component from prior T1");
|
|
647
|
+
// Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit.
|
|
648
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([{ type: "text", text: "C" }, t2]) });
|
|
649
|
+
});
|
|
650
|
+
// Regression: after a sub-turn shrink, lastPinnedText must be cleared so the
|
|
651
|
+
// pinned "Latest Output" mirror can display text from the new sub-turn instead
|
|
652
|
+
// of staying frozen on a stale snapshot (the "bottom green stays" symptom).
|
|
653
|
+
test("chat-controller updates pinned zone after sub-turn shrink", async () => {
|
|
654
|
+
globalThis[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
655
|
+
fg: (_key, text) => text,
|
|
656
|
+
bg: (_key, text) => text,
|
|
657
|
+
bold: (text) => text,
|
|
658
|
+
italic: (text) => text,
|
|
659
|
+
truncate: (text) => text,
|
|
660
|
+
};
|
|
661
|
+
const host = createHost();
|
|
662
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
663
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
664
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
665
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) });
|
|
666
|
+
// Sub-turn 1 with pinnable text before a tool → populates pinned zone with "first".
|
|
667
|
+
await handleAgentEvent(host, {
|
|
668
|
+
type: "message_update",
|
|
669
|
+
message: makeAssistant([{ type: "text", text: "first" }, t1]),
|
|
670
|
+
assistantMessageEvent: {
|
|
671
|
+
type: "toolcall_end", contentIndex: 1,
|
|
672
|
+
toolCall: { ...t1, externalResult: { content: [{ type: "text", text: "r1" }], details: {}, isError: false } },
|
|
673
|
+
partial: makeAssistant([{ type: "text", text: "first" }, t1]),
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
const pinnedMarkdown = host.pinnedMessageContainer.children[1];
|
|
677
|
+
assert.equal(pinnedMarkdown?.text, "first", "pinned zone seeded with sub-turn 1 text");
|
|
678
|
+
// Sub-turn boundary: content resets to [second, t2].
|
|
679
|
+
await handleAgentEvent(host, {
|
|
680
|
+
type: "message_update",
|
|
681
|
+
message: makeAssistant([{ type: "text", text: "second" }, t2]),
|
|
682
|
+
assistantMessageEvent: {
|
|
683
|
+
type: "toolcall_end", contentIndex: 1,
|
|
684
|
+
toolCall: { ...t2, externalResult: { content: [{ type: "text", text: "r2" }], details: {}, isError: false } },
|
|
685
|
+
partial: makeAssistant([{ type: "text", text: "second" }, t2]),
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
// Pinned markdown must now reflect the new sub-turn's text, not stay frozen on "first".
|
|
689
|
+
assert.equal(pinnedMarkdown?.text, "second", "pinned zone must update after sub-turn shrink (#4144 regression)");
|
|
690
|
+
// Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit.
|
|
691
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([{ type: "text", text: "second" }, t2]) });
|
|
692
|
+
});
|
|
388
693
|
//# sourceMappingURL=chat-controller-ordering.test.js.map
|