@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/cli.js +3624 -3568
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +144 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +106 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -5
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +867 -428
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Input,
|
|
10
10
|
matchesKey,
|
|
11
11
|
ScrollView,
|
|
12
|
+
type SgrMouseEvent,
|
|
12
13
|
Spacer,
|
|
13
14
|
type Tab,
|
|
14
15
|
TabBar,
|
|
@@ -174,7 +175,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
174
175
|
#tui: TUI;
|
|
175
176
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
176
177
|
#temporaryOnly: boolean;
|
|
178
|
+
#directSelect: boolean;
|
|
179
|
+
#pickerHint: string | undefined;
|
|
177
180
|
#currentContextTokens: number;
|
|
181
|
+
#listLineOffset = 0;
|
|
182
|
+
#listStartIndex = 0;
|
|
183
|
+
#listVisibleCount = 0;
|
|
178
184
|
|
|
179
185
|
#menuRoleActions: MenuRoleAction[] = [];
|
|
180
186
|
|
|
@@ -200,7 +206,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
200
206
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
201
207
|
onSelect: RoleSelectCallback,
|
|
202
208
|
onCancel: () => void,
|
|
203
|
-
options?: {
|
|
209
|
+
options?: {
|
|
210
|
+
temporaryOnly?: boolean;
|
|
211
|
+
directSelect?: boolean;
|
|
212
|
+
pickerHint?: string;
|
|
213
|
+
initialSearchInput?: string;
|
|
214
|
+
currentContextTokens?: number;
|
|
215
|
+
},
|
|
204
216
|
) {
|
|
205
217
|
super();
|
|
206
218
|
|
|
@@ -211,6 +223,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
211
223
|
this.#onSelectCallback = onSelect;
|
|
212
224
|
this.#onCancelCallback = onCancel;
|
|
213
225
|
this.#temporaryOnly = options?.temporaryOnly ?? false;
|
|
226
|
+
this.#directSelect = options?.directSelect ?? false;
|
|
227
|
+
this.#pickerHint = options?.pickerHint;
|
|
214
228
|
const currentContextTokens = options?.currentContextTokens ?? 0;
|
|
215
229
|
this.#currentContextTokens =
|
|
216
230
|
Number.isFinite(currentContextTokens) && currentContextTokens > 0 ? Math.floor(currentContextTokens) : 0;
|
|
@@ -236,6 +250,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
236
250
|
if (this.#temporaryOnly) {
|
|
237
251
|
this.addChild(new Text(theme.fg("muted", TEMPORARY_MODEL_PICKER_HINT), 0, 0));
|
|
238
252
|
this.addChild(new Spacer(1));
|
|
253
|
+
} else if (this.#directSelect && this.#pickerHint) {
|
|
254
|
+
this.addChild(new Text(theme.fg("muted", this.#pickerHint), 0, 0));
|
|
255
|
+
this.addChild(new Spacer(1));
|
|
239
256
|
}
|
|
240
257
|
|
|
241
258
|
// Create header container for tab bar
|
|
@@ -714,10 +731,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
714
731
|
return this.#temporaryOnly && this.#isModelOverCurrentContext(model);
|
|
715
732
|
}
|
|
716
733
|
|
|
717
|
-
#isDefaultRoleActionOverContextLimit(action: MenuRoleAction, model: Model): boolean {
|
|
718
|
-
return action.role === "default" && this.#isModelOverCurrentContext(model);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
734
|
#formatCurrentContextLimitSuffix(model: Model): string {
|
|
722
735
|
return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow ?? 0).toLowerCase()}`;
|
|
723
736
|
}
|
|
@@ -937,6 +950,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
937
950
|
Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), visibleItems.length - maxVisible),
|
|
938
951
|
);
|
|
939
952
|
const endIndex = Math.min(startIndex + maxVisible, visibleItems.length);
|
|
953
|
+
this.#listStartIndex = startIndex;
|
|
954
|
+
this.#listVisibleCount = Math.max(0, endIndex - startIndex);
|
|
940
955
|
|
|
941
956
|
const showProvider = this.#getActiveTabId() === ALL_TAB;
|
|
942
957
|
|
|
@@ -1065,47 +1080,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
1065
1080
|
: this.#filteredModels[this.#selectedIndex];
|
|
1066
1081
|
}
|
|
1067
1082
|
|
|
1068
|
-
#
|
|
1069
|
-
const action = this.#menuRoleActions[index];
|
|
1070
|
-
return action ? this.#isDefaultRoleActionOverContextLimit(action, selectedItem.model) : false;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
#coerceMenuSelectedIndex(index: number, selectedItem: ModelItem | CanonicalModelItem): number {
|
|
1083
|
+
#coerceMenuSelectedIndex(index: number): number {
|
|
1074
1084
|
const maxIndex = this.#menuRoleActions.length - 1;
|
|
1075
1085
|
if (maxIndex < 0) {
|
|
1076
1086
|
return 0;
|
|
1077
1087
|
}
|
|
1078
|
-
|
|
1079
|
-
if (!this.#isMenuRoleIndexDisabled(clamped, selectedItem)) {
|
|
1080
|
-
return clamped;
|
|
1081
|
-
}
|
|
1082
|
-
for (let i = clamped + 1; i <= maxIndex; i++) {
|
|
1083
|
-
if (!this.#isMenuRoleIndexDisabled(i, selectedItem)) {
|
|
1084
|
-
return i;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
for (let i = clamped - 1; i >= 0; i--) {
|
|
1088
|
-
if (!this.#isMenuRoleIndexDisabled(i, selectedItem)) {
|
|
1089
|
-
return i;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
return clamped;
|
|
1088
|
+
return Math.max(0, Math.min(index, maxIndex));
|
|
1093
1089
|
}
|
|
1094
1090
|
|
|
1095
|
-
#moveMenuSelection(delta: number,
|
|
1096
|
-
|
|
1097
|
-
for (let step = 0; step < optionCount; step++) {
|
|
1098
|
-
index = (index + delta + optionCount) % optionCount;
|
|
1099
|
-
if (this.#menuStep !== "role" || !this.#isMenuRoleIndexDisabled(index, selectedItem)) {
|
|
1100
|
-
this.#menuSelectedIndex = index;
|
|
1101
|
-
this.#updateMenu();
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
this.#menuSelectedIndex =
|
|
1106
|
-
this.#menuStep === "role"
|
|
1107
|
-
? this.#coerceMenuSelectedIndex(this.#menuSelectedIndex, selectedItem)
|
|
1108
|
-
: this.#menuSelectedIndex;
|
|
1091
|
+
#moveMenuSelection(delta: number, _selectedItem: ModelItem | CanonicalModelItem, optionCount: number): void {
|
|
1092
|
+
this.#menuSelectedIndex = (this.#menuSelectedIndex + delta + optionCount) % optionCount;
|
|
1109
1093
|
this.#updateMenu();
|
|
1110
1094
|
}
|
|
1111
1095
|
|
|
@@ -1116,7 +1100,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1116
1100
|
this.#isMenuOpen = true;
|
|
1117
1101
|
this.#menuStep = "role";
|
|
1118
1102
|
this.#menuSelectedRole = null;
|
|
1119
|
-
this.#menuSelectedIndex = this.#coerceMenuSelectedIndex(0
|
|
1103
|
+
this.#menuSelectedIndex = this.#coerceMenuSelectedIndex(0);
|
|
1120
1104
|
// Collapse the model list while the action/thinking menu is open so the
|
|
1121
1105
|
// menu owns the full viewport instead of stacking below a now-irrelevant
|
|
1122
1106
|
// (and often off-screen) list.
|
|
@@ -1149,9 +1133,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1149
1133
|
})
|
|
1150
1134
|
: this.#menuRoleActions.map((action, index) => {
|
|
1151
1135
|
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
|
1152
|
-
|
|
1153
|
-
const suffix = disabled ? this.#formatCurrentContextLimitSuffix(selectedItem.model) : "";
|
|
1154
|
-
return `${prefix}${action.label}${suffix}`;
|
|
1136
|
+
return `${prefix}${action.label}`;
|
|
1155
1137
|
});
|
|
1156
1138
|
|
|
1157
1139
|
const selectedRoleName = this.#menuSelectedRole ? getRoleInfo(this.#menuSelectedRole, this.#settings).name : "";
|
|
@@ -1231,6 +1213,55 @@ export class ModelSelectorComponent extends Container {
|
|
|
1231
1213
|
return Math.max(MIN_VISIBLE_OPTIONS, Math.min(optionCount, terminalRows - MENU_CHROME_ROWS));
|
|
1232
1214
|
}
|
|
1233
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Concatenate children like Container.render, recording where the model list
|
|
1218
|
+
* lands so routed mouse events can be hit-tested against it.
|
|
1219
|
+
*/
|
|
1220
|
+
override render(width: number): readonly string[] {
|
|
1221
|
+
const lines: string[] = [];
|
|
1222
|
+
for (const child of this.children) {
|
|
1223
|
+
const childLines = child.render(Math.max(1, width));
|
|
1224
|
+
if (child === this.#listContainer) {
|
|
1225
|
+
this.#listLineOffset = lines.length;
|
|
1226
|
+
}
|
|
1227
|
+
lines.push(...childLines);
|
|
1228
|
+
}
|
|
1229
|
+
return lines;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
1233
|
+
if (this.#isMenuOpen) return;
|
|
1234
|
+
|
|
1235
|
+
if (event.wheel !== null) {
|
|
1236
|
+
this.#moveSelection(event.wheel);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const listLine = line - this.#listLineOffset;
|
|
1241
|
+
if (listLine < 0 || listLine >= this.#listVisibleCount) return;
|
|
1242
|
+
|
|
1243
|
+
const index = this.#listStartIndex + listLine;
|
|
1244
|
+
const item = this.#getVisibleItems()[index];
|
|
1245
|
+
if (!item || this.#isItemDisabled(item)) return;
|
|
1246
|
+
|
|
1247
|
+
if (event.motion) {
|
|
1248
|
+
if (index !== this.#selectedIndex) {
|
|
1249
|
+
this.#selectedIndex = index;
|
|
1250
|
+
this.#updateList();
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (event.leftClick) {
|
|
1256
|
+
this.#selectedIndex = index;
|
|
1257
|
+
if (this.#temporaryOnly || this.#directSelect) {
|
|
1258
|
+
this.#handleSelect(item, null);
|
|
1259
|
+
} else {
|
|
1260
|
+
this.#openMenu();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1234
1265
|
handleInput(keyData: string): void {
|
|
1235
1266
|
if (this.#isMenuOpen) {
|
|
1236
1267
|
this.#handleMenuInput(keyData);
|
|
@@ -1254,12 +1285,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
1254
1285
|
return;
|
|
1255
1286
|
}
|
|
1256
1287
|
|
|
1257
|
-
// Enter - open context menu or select directly in temporary mode
|
|
1288
|
+
// Enter - open context menu or select directly in temporary/direct-select mode
|
|
1258
1289
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1259
1290
|
const selectedItem = this.#getSelectedItem();
|
|
1260
1291
|
if (selectedItem && !this.#isItemDisabled(selectedItem)) {
|
|
1261
|
-
if (this.#temporaryOnly) {
|
|
1262
|
-
// In temporary mode, skip menu and select directly
|
|
1292
|
+
if (this.#temporaryOnly || this.#directSelect) {
|
|
1293
|
+
// In temporary/direct-select mode, skip menu and select directly
|
|
1263
1294
|
this.#handleSelect(selectedItem, null);
|
|
1264
1295
|
} else {
|
|
1265
1296
|
this.#openMenu();
|
|
@@ -1301,7 +1332,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1301
1332
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1302
1333
|
if (this.#menuStep === "role") {
|
|
1303
1334
|
const action = this.#menuRoleActions[this.#menuSelectedIndex];
|
|
1304
|
-
if (!action
|
|
1335
|
+
if (!action) return;
|
|
1305
1336
|
this.#menuSelectedRole = action.role;
|
|
1306
1337
|
this.#menuStep = "thinking";
|
|
1307
1338
|
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
@@ -162,6 +162,29 @@ interface ActiveRepoCache {
|
|
|
162
162
|
effectiveGitCwd: string;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Per-{@link AgentSession} active-processing meter for the `time_spent`
|
|
167
|
+
* segment. `activeMs` is the union of every completed `agent_start`→
|
|
168
|
+
* `agent_end` window; `activeStartedAt` is the start timestamp of the
|
|
169
|
+
* currently-running window, or `null` when idle.
|
|
170
|
+
*
|
|
171
|
+
* `sessionFile` snapshots the loaded session-file path at meter-creation
|
|
172
|
+
* time. `AgentSession.switchSession` (/resume, /move, ACP fork, RPC
|
|
173
|
+
* `switch_session`, extension `switchSession`) mutates the loaded file
|
|
174
|
+
* under the same {@link AgentSession} ref, so the WeakMap key alone
|
|
175
|
+
* cannot tell two conversations apart. `#meter()` compares this snapshot
|
|
176
|
+
* against the live `session.sessionFile`, and a real-to-real change
|
|
177
|
+
* starts the meter fresh instead of crediting the new conversation with
|
|
178
|
+
* the previous one's accumulated active time. The undefined → real
|
|
179
|
+
* first-save transition does not reset, since the session identity has
|
|
180
|
+
* not changed.
|
|
181
|
+
*/
|
|
182
|
+
interface ActiveMeter {
|
|
183
|
+
activeMs: number;
|
|
184
|
+
activeStartedAt: number | null;
|
|
185
|
+
sessionFile: string | undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
const EMPTY_MESSAGES: readonly AgentMessage[] = [];
|
|
166
189
|
const STATUS_USAGE_START_DELAY_MS = 0;
|
|
167
190
|
const STATUS_USAGE_REFRESH_TIMEOUT_MS = 2_000;
|
|
@@ -200,7 +223,25 @@ export class StatusLineComponent implements Component {
|
|
|
200
223
|
#autoCompactEnabled: boolean = true;
|
|
201
224
|
#hookStatuses: Map<string, string> = new Map();
|
|
202
225
|
#subagentCount: number = 0;
|
|
203
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Active-processing accounting for the `time_spent` segment, keyed per
|
|
228
|
+
* {@link AgentSession} so the focus-controller mid-turn attach path
|
|
229
|
+
* cannot leak an unmatched synthesized `agent_start` from a subagent
|
|
230
|
+
* into the main session's meter.
|
|
231
|
+
*
|
|
232
|
+
* Each meter is `{ activeMs, activeStartedAt }`: `activeMs` is the union
|
|
233
|
+
* of every completed `agent_start`→`agent_end` window since
|
|
234
|
+
* {@link resetActiveTime} last reset it; `activeStartedAt` is the start
|
|
235
|
+
* timestamp of the currently-running window (or `null` when idle).
|
|
236
|
+
* `getActiveMs()` returns `activeMs + (now - activeStartedAt)` for the
|
|
237
|
+
* currently-attached session, so the counter ticks live during a turn
|
|
238
|
+
* and freezes the instant the agent yields.
|
|
239
|
+
*
|
|
240
|
+
* WeakMap so meters die with their session (e.g. a parked subagent
|
|
241
|
+
* dropped from the registry); the main session's meter survives focus
|
|
242
|
+
* round-trips because the same {@link AgentSession} ref is reused.
|
|
243
|
+
*/
|
|
244
|
+
#activeMeters: WeakMap<AgentSession, ActiveMeter> = new WeakMap();
|
|
204
245
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
205
246
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
206
247
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
@@ -250,6 +291,7 @@ export class StatusLineComponent implements Component {
|
|
|
250
291
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
251
292
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
252
293
|
transparent: settings.get("statusLine.transparent"),
|
|
294
|
+
compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
|
|
253
295
|
};
|
|
254
296
|
}
|
|
255
297
|
#gitEnabled(): boolean {
|
|
@@ -283,10 +325,30 @@ export class StatusLineComponent implements Component {
|
|
|
283
325
|
if (!sessionChanged && this.#focusedAgentId === focusedAgentId) return;
|
|
284
326
|
this.session = session;
|
|
285
327
|
this.#focusedAgentId = focusedAgentId;
|
|
286
|
-
if (sessionChanged)
|
|
328
|
+
if (sessionChanged) {
|
|
329
|
+
this.#invalidateSessionCaches();
|
|
330
|
+
this.#closeStaleActiveWindow();
|
|
331
|
+
}
|
|
287
332
|
this.invalidate();
|
|
288
333
|
}
|
|
289
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Drop a meter's in-flight window when the newly-attached session is no
|
|
337
|
+
* longer streaming. Handles the case where the focus controller
|
|
338
|
+
* synthesized an `agent_start` on a mid-turn attach but the matching
|
|
339
|
+
* real `agent_end` never reached us — the user detached before it
|
|
340
|
+
* fired, and re-focusing later (after the agent finished) would
|
|
341
|
+
* otherwise tick over the entire detached gap. Crediting that gap to
|
|
342
|
+
* `activeMs` would be wrong (the agent finished at some point we never
|
|
343
|
+
* observed), so the window is dropped rather than folded in.
|
|
344
|
+
*/
|
|
345
|
+
#closeStaleActiveWindow(): void {
|
|
346
|
+
const meter = this.#meter();
|
|
347
|
+
if (meter.activeStartedAt === null) return;
|
|
348
|
+
if (this.session.isStreaming) return;
|
|
349
|
+
meter.activeStartedAt = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
290
352
|
updateSettings(settings: StatusLineSettings): void {
|
|
291
353
|
this.#settings = settings;
|
|
292
354
|
this.#effectiveSettings = undefined;
|
|
@@ -310,8 +372,84 @@ export class StatusLineComponent implements Component {
|
|
|
310
372
|
return this.#subagentCount;
|
|
311
373
|
}
|
|
312
374
|
|
|
313
|
-
|
|
314
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Reset the currently-attached session's active-time accumulators so
|
|
377
|
+
* the `time_spent` segment starts from zero. Called from `/clear`,
|
|
378
|
+
* fresh-session, and joined-collab paths; both the completed
|
|
379
|
+
* accumulator and any in-flight window are dropped, so a reset
|
|
380
|
+
* mid-turn ignores the running window (the matching `markActivityEnd`
|
|
381
|
+
* will see an idle meter and no-op).
|
|
382
|
+
*/
|
|
383
|
+
resetActiveTime(): void {
|
|
384
|
+
const meter = this.#meter();
|
|
385
|
+
meter.activeMs = 0;
|
|
386
|
+
meter.activeStartedAt = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Mark the currently-attached session as having started a unit of
|
|
391
|
+
* active processing. Idempotent: a second start while a window is
|
|
392
|
+
* already open is a no-op, so reentrant `agent_start` events (e.g.
|
|
393
|
+
* nested auto-compaction loops, focus-controller mid-turn attach onto
|
|
394
|
+
* an already-running window) do not double-count.
|
|
395
|
+
*/
|
|
396
|
+
markActivityStart(): void {
|
|
397
|
+
const meter = this.#meter();
|
|
398
|
+
if (meter.activeStartedAt !== null) return;
|
|
399
|
+
meter.activeStartedAt = Date.now();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Close the currently-attached session's open active-processing
|
|
404
|
+
* window, folding its elapsed time into the accumulator. Idempotent
|
|
405
|
+
* when the meter is already idle so callers can fire it on every
|
|
406
|
+
* `agent_end` without guarding.
|
|
407
|
+
*/
|
|
408
|
+
markActivityEnd(): void {
|
|
409
|
+
const meter = this.#meter();
|
|
410
|
+
if (meter.activeStartedAt === null) return;
|
|
411
|
+
meter.activeMs += Math.max(0, Date.now() - meter.activeStartedAt);
|
|
412
|
+
meter.activeStartedAt = null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Snapshot of total active-processing time for the currently-attached
|
|
417
|
+
* session, including any in-flight window. Exposed for the segment
|
|
418
|
+
* context builder; tests assert against this too.
|
|
419
|
+
*/
|
|
420
|
+
getActiveMs(): number {
|
|
421
|
+
const meter = this.#meter();
|
|
422
|
+
if (meter.activeStartedAt === null) return meter.activeMs;
|
|
423
|
+
return meter.activeMs + Math.max(0, Date.now() - meter.activeStartedAt);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Return (lazily creating) the meter for the currently-attached
|
|
428
|
+
* session. Detects an in-place session-file swap under the same
|
|
429
|
+
* {@link AgentSession} ref (`switchSession` paths: `/resume`, `/move`,
|
|
430
|
+
* ACP fork/load, RPC `switch_session`, extension `switchSession`):
|
|
431
|
+
* a real-to-real change starts the meter fresh so the new
|
|
432
|
+
* conversation does not inherit the previous one's accumulated active
|
|
433
|
+
* time. The undefined → real first-save transition only refreshes the
|
|
434
|
+
* snapshot — the conversation identity has not changed.
|
|
435
|
+
*/
|
|
436
|
+
#meter(): ActiveMeter {
|
|
437
|
+
const currentFile = this.session.sessionFile;
|
|
438
|
+
let meter = this.#activeMeters.get(this.session);
|
|
439
|
+
if (meter) {
|
|
440
|
+
const switched =
|
|
441
|
+
currentFile !== undefined && meter.sessionFile !== undefined && meter.sessionFile !== currentFile;
|
|
442
|
+
if (switched) {
|
|
443
|
+
meter = undefined;
|
|
444
|
+
} else {
|
|
445
|
+
meter.sessionFile = currentFile;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!meter) {
|
|
449
|
+
meter = { activeMs: 0, activeStartedAt: null, sessionFile: currentFile };
|
|
450
|
+
this.#activeMeters.set(this.session, meter);
|
|
451
|
+
}
|
|
452
|
+
return meter;
|
|
315
453
|
}
|
|
316
454
|
|
|
317
455
|
setPlanModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
|
|
@@ -854,6 +992,7 @@ export class StatusLineComponent implements Component {
|
|
|
854
992
|
activeRepo: activeRepoCache.activeRepo,
|
|
855
993
|
width,
|
|
856
994
|
options: segmentOptions ?? {},
|
|
995
|
+
compactThinkingLevel: this.#resolveSettings().compactThinkingLevel ?? false,
|
|
857
996
|
planMode: this.#planModeStatus,
|
|
858
997
|
loopMode: this.#loopModeStatus,
|
|
859
998
|
goalMode: this.#goalModeStatus,
|
|
@@ -864,7 +1003,7 @@ export class StatusLineComponent implements Component {
|
|
|
864
1003
|
contextWindow,
|
|
865
1004
|
autoCompactEnabled: this.#autoCompactEnabled,
|
|
866
1005
|
subagentCount: this.#subagentCount,
|
|
867
|
-
|
|
1006
|
+
activeMs: this.getActiveMs(),
|
|
868
1007
|
git: {
|
|
869
1008
|
branch: gitBranch,
|
|
870
1009
|
status: gitStatus,
|
|
@@ -20,6 +20,16 @@ function withIcon(icon: string, text: string): string {
|
|
|
20
20
|
return icon ? `${icon} ${text}` : text;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Leading glyph of a thinking-level display string (e.g. "◉ xhigh" → "◉").
|
|
25
|
+
* Compact mode promotes this glyph to the model-segment icon so the level
|
|
26
|
+
* stays visible without the verbose " · <level>" tail.
|
|
27
|
+
*/
|
|
28
|
+
function thinkingGlyph(display: string): string {
|
|
29
|
+
const space = display.indexOf(" ");
|
|
30
|
+
return space === -1 ? display : display.slice(0, space);
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
function stripDisplayRoot(pwd: string): string {
|
|
24
34
|
for (const root of [path.join(os.homedir(), "Projects"), "/work"]) {
|
|
25
35
|
const relative = relativePathWithinRoot(root, pwd);
|
|
@@ -86,37 +96,46 @@ const modelSegment: StatusLineSegment = {
|
|
|
86
96
|
modelName = modelName.slice(7);
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
// distinct marker. theme.fg resets only the fg, so the spans are
|
|
93
|
-
// concatenated (not nested) to keep each color intact.
|
|
94
|
-
let tail = "";
|
|
95
|
-
if (ctx.session.isFastModeActive() && theme.icon.fast) {
|
|
96
|
-
tail += ` ${theme.icon.fast}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
+
// Resolve the current thinking-level display ("◉ xhigh", "⟳ auto", …)
|
|
100
|
+
// when the model supports thinking and the segment isn't hiding it.
|
|
101
|
+
let thinkingDisplay = "";
|
|
99
102
|
if (opts.showThinkingLevel !== false && state.model?.thinking) {
|
|
100
103
|
if (ctx.session.isAutoThinking) {
|
|
101
104
|
// Pending (no turn classified yet / classifying) shows a symbol-theme
|
|
102
105
|
// question-box marker; once resolved it shows `<level>`.
|
|
103
106
|
const resolved = ctx.session.autoResolvedThinkingLevel();
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
thinkingDisplay = resolved
|
|
108
|
+
? (theme.thinking[resolved as keyof typeof theme.thinking] ?? resolved)
|
|
109
|
+
: `${theme.thinking.autoPending} auto`;
|
|
106
110
|
} else {
|
|
107
111
|
const level = state.thinkingLevel ?? ThinkingLevel.Off;
|
|
108
112
|
if (level !== ThinkingLevel.Off) {
|
|
109
|
-
|
|
110
|
-
if (thinkingText) {
|
|
111
|
-
tail += `${theme.sep.dot}${thinkingText}`;
|
|
112
|
-
}
|
|
113
|
+
thinkingDisplay = theme.thinking[level as keyof typeof theme.thinking] ?? "";
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Compact mode swaps the model icon for the thinking-level glyph and drops
|
|
119
|
+
// the " · <level>" tail, keeping the level visible as a single icon.
|
|
120
|
+
const compact = ctx.compactThinkingLevel && thinkingDisplay !== "";
|
|
121
|
+
const modelIcon = compact ? thinkingGlyph(thinkingDisplay) : theme.icon.model;
|
|
122
|
+
|
|
123
|
+
// Fast-mode icon and thinking-level suffix trail the model name and are
|
|
124
|
+
// colored together with it as `statusLineModel`. The advisor "++" badge
|
|
125
|
+
// sits between the name and that tail in `accent`, so it reads as a
|
|
126
|
+
// distinct marker. theme.fg resets only the fg, so the spans are
|
|
127
|
+
// concatenated (not nested) to keep each color intact.
|
|
128
|
+
let tail = "";
|
|
129
|
+
if (ctx.session.isFastModeActive() && theme.icon.fast) {
|
|
130
|
+
tail += ` ${theme.icon.fast}`;
|
|
131
|
+
}
|
|
132
|
+
if (!compact && thinkingDisplay) {
|
|
133
|
+
tail += `${theme.sep.dot}${thinkingDisplay}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
// `statusLineModel` is aliased to `accent` in many themes, so the badge
|
|
118
137
|
// uses `success` to stay visibly distinct from the model name color.
|
|
119
|
-
let content = theme.fg("statusLineModel", withIcon(
|
|
138
|
+
let content = theme.fg("statusLineModel", withIcon(modelIcon, modelName));
|
|
120
139
|
if (ctx.session.isAdvisorActive()) {
|
|
121
140
|
content += theme.fg("success", "++");
|
|
122
141
|
}
|
|
@@ -400,13 +419,19 @@ const contextTotalSegment: StatusLineSegment = {
|
|
|
400
419
|
},
|
|
401
420
|
};
|
|
402
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Total time the agent was actively processing this session — the union of
|
|
424
|
+
* every `agent_start`→`agent_end` window plus the currently-running window,
|
|
425
|
+
* sourced from {@link SegmentContext.activeMs}. Idle wall-clock between turns
|
|
426
|
+
* never accumulates, so the displayed total reflects how long the agent has
|
|
427
|
+
* been working for the user, not how long the session has been open. Hidden
|
|
428
|
+
* before the first second of activity to avoid flashing `0s` at session start.
|
|
429
|
+
*/
|
|
403
430
|
const timeSpentSegment: StatusLineSegment = {
|
|
404
431
|
id: "time_spent",
|
|
405
432
|
render(ctx) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return { content: withIcon(theme.icon.time, formatDuration(elapsed)), visible: true };
|
|
433
|
+
if (ctx.activeMs < 1000) return { content: "", visible: false };
|
|
434
|
+
return { content: withIcon(theme.icon.time, formatDuration(ctx.activeMs)), visible: true };
|
|
410
435
|
},
|
|
411
436
|
};
|
|
412
437
|
|
|
@@ -31,6 +31,9 @@ export interface StatusLineSettings {
|
|
|
31
31
|
/** Drop the theme's `statusLineBg` fill and powerline caps so the bar
|
|
32
32
|
* inherits the terminal's default background. */
|
|
33
33
|
transparent?: boolean;
|
|
34
|
+
/** Replace the model-segment icon with the thinking-level glyph and drop the
|
|
35
|
+
* " · <level>" suffix, so the thinking level reads as a single compact icon. */
|
|
36
|
+
compactThinkingLevel?: boolean;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export type EffectiveStatusLineSettings = Required<
|
|
@@ -51,6 +54,8 @@ export interface SegmentContext {
|
|
|
51
54
|
activeRepo: ActiveRepoContext | null;
|
|
52
55
|
width: number;
|
|
53
56
|
options: StatusLineSegmentOptions;
|
|
57
|
+
/** Render the model segment's thinking level as a compact leading glyph. */
|
|
58
|
+
compactThinkingLevel: boolean;
|
|
54
59
|
planMode: {
|
|
55
60
|
enabled: boolean;
|
|
56
61
|
paused: boolean;
|
|
@@ -79,7 +84,14 @@ export interface SegmentContext {
|
|
|
79
84
|
contextWindow: number;
|
|
80
85
|
autoCompactEnabled: boolean;
|
|
81
86
|
subagentCount: number;
|
|
82
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Active processing time accumulated this session, in ms — the union of
|
|
89
|
+
* every `agent_start`→`agent_end` window plus the currently-streaming
|
|
90
|
+
* window if the agent is running. Idle wall-clock never contributes, so
|
|
91
|
+
* this is what {@link StatusLineSegmentId.time_spent} renders instead of
|
|
92
|
+
* `Date.now() - sessionStart`.
|
|
93
|
+
*/
|
|
94
|
+
activeMs: number;
|
|
83
95
|
git: {
|
|
84
96
|
branch: string | null;
|
|
85
97
|
status: { staged: number; unstaged: number; untracked: number } | null;
|
|
@@ -132,6 +132,14 @@ function rawTextInputFromPartialJson(partialJson: unknown): string | undefined {
|
|
|
132
132
|
return partialJson;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** Read the streamed raw-JSON buffer a tool block stashes on its args, narrowed
|
|
136
|
+
* rather than cast: a missing or non-string `__partialJson` yields `undefined`. */
|
|
137
|
+
function partialJsonOf(args: unknown): string | undefined {
|
|
138
|
+
if (args == null || typeof args !== "object" || !("__partialJson" in args)) return undefined;
|
|
139
|
+
const value = args.__partialJson;
|
|
140
|
+
return typeof value === "string" ? value : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
function getArgsWithStreamedTextInput(args: unknown): unknown {
|
|
136
144
|
if (args == null || typeof args !== "object") return args;
|
|
137
145
|
const record = args as Record<string, unknown>;
|
|
@@ -242,6 +250,10 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
242
250
|
#editDiffLastArgsKey?: string;
|
|
243
251
|
// Latest in-flight streaming diff recompute, captured so it can be awaited.
|
|
244
252
|
#editDiffInFlight?: Promise<void>;
|
|
253
|
+
/** Set when newer args arrived while a preview compute was in flight; the
|
|
254
|
+
* drain loop re-runs once the current compute settles, so a slow diff
|
|
255
|
+
* coalesces streamed ticks instead of being aborted by each one. */
|
|
256
|
+
#editDiffDirty = false;
|
|
245
257
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
246
258
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
247
259
|
// Spinner animation for partial task results
|
|
@@ -323,7 +335,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
323
335
|
this.setIgnoreTight(true);
|
|
324
336
|
|
|
325
337
|
this.#updateDisplay();
|
|
326
|
-
this.#
|
|
338
|
+
this.#schedulePreviewDiff();
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
updateArgs(args: any, _toolCallId?: string): void {
|
|
@@ -335,7 +347,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
335
347
|
this.#args = args;
|
|
336
348
|
this.#displayInputVersion++;
|
|
337
349
|
this.#updateSpinnerAnimation();
|
|
338
|
-
this.#
|
|
350
|
+
this.#schedulePreviewDiff();
|
|
339
351
|
this.#updateDisplay();
|
|
340
352
|
}
|
|
341
353
|
|
|
@@ -346,7 +358,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
346
358
|
setArgsComplete(_toolCallId?: string): void {
|
|
347
359
|
this.#argsComplete = true;
|
|
348
360
|
this.#updateSpinnerAnimation();
|
|
349
|
-
this.#
|
|
361
|
+
this.#schedulePreviewDiff();
|
|
350
362
|
}
|
|
351
363
|
|
|
352
364
|
/**
|
|
@@ -360,7 +372,32 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
360
372
|
await this.#editDiffInFlight;
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Schedule a streaming diff preview recompute, coalescing bursts of
|
|
377
|
+
* `updateArgs` into one compute at a time: run the current compute to
|
|
378
|
+
* completion and re-run only after it settles when newer args arrived, never
|
|
379
|
+
* cancelling an in-flight compute on a fresh tick. The reveal controller pushes
|
|
380
|
+
* args ~30fps and a whole-file hashline/large-file diff can outlast a frame, so
|
|
381
|
+
* cancel-per-tick would starve every compute and no preview would land until
|
|
382
|
+
* args complete. Coalescing lets each diff land, so the preview tracks the
|
|
383
|
+
* stream at the rate the diffs can sustain.
|
|
384
|
+
*/
|
|
385
|
+
#schedulePreviewDiff(): void {
|
|
386
|
+
this.#editDiffDirty = true;
|
|
387
|
+
if (this.#editDiffInFlight) return;
|
|
388
|
+
this.#editDiffInFlight = this.#drainPreviewDiff().finally(() => {
|
|
389
|
+
this.#editDiffInFlight = undefined;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async #drainPreviewDiff(): Promise<void> {
|
|
394
|
+
while (this.#editDiffDirty) {
|
|
395
|
+
this.#editDiffDirty = false;
|
|
396
|
+
await this.#computePreviewDiff();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async #computePreviewDiff(): Promise<void> {
|
|
364
401
|
const editMode = this.#editMode;
|
|
365
402
|
if (!editMode) return;
|
|
366
403
|
const strategy = EDIT_MODE_STRATEGIES[editMode];
|
|
@@ -370,7 +407,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
370
407
|
if (args == null || typeof args !== "object") return;
|
|
371
408
|
|
|
372
409
|
const previewArgs = getArgsWithStreamedTextInput(args);
|
|
373
|
-
const partialJson = (previewArgs
|
|
410
|
+
const partialJson = partialJsonOf(previewArgs);
|
|
374
411
|
let effectiveArgs: unknown;
|
|
375
412
|
try {
|
|
376
413
|
effectiveArgs = strategy.extractCompleteEdits(previewArgs, partialJson);
|
|
@@ -400,7 +437,8 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
400
437
|
if (argsKey === this.#editDiffLastArgsKey) return;
|
|
401
438
|
this.#editDiffLastArgsKey = argsKey;
|
|
402
439
|
|
|
403
|
-
|
|
440
|
+
// Single-flight (the drain loop never overlaps computes), so this controller
|
|
441
|
+
// only ever cancels the live compute on teardown via `stopAnimation`.
|
|
404
442
|
const controller = new AbortController();
|
|
405
443
|
this.#editDiffAbort = controller;
|
|
406
444
|
|
|
@@ -732,6 +770,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
732
770
|
this.#stopTodoStrikeAnimation();
|
|
733
771
|
this.#editDiffAbort?.abort();
|
|
734
772
|
this.#editDiffAbort = undefined;
|
|
773
|
+
// Drop any queued rerun so the drain loop exits instead of recomputing a
|
|
774
|
+
// preview for a torn-down block after its in-flight compute is aborted.
|
|
775
|
+
this.#editDiffDirty = false;
|
|
735
776
|
}
|
|
736
777
|
|
|
737
778
|
setExpanded(expanded: boolean): void {
|