@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -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,8 +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
|
-
|
|
204
|
-
|
|
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();
|
|
205
245
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
206
246
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
207
247
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
@@ -251,6 +291,7 @@ export class StatusLineComponent implements Component {
|
|
|
251
291
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
252
292
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
253
293
|
transparent: settings.get("statusLine.transparent"),
|
|
294
|
+
compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
|
|
254
295
|
};
|
|
255
296
|
}
|
|
256
297
|
#gitEnabled(): boolean {
|
|
@@ -284,10 +325,30 @@ export class StatusLineComponent implements Component {
|
|
|
284
325
|
if (!sessionChanged && this.#focusedAgentId === focusedAgentId) return;
|
|
285
326
|
this.session = session;
|
|
286
327
|
this.#focusedAgentId = focusedAgentId;
|
|
287
|
-
if (sessionChanged)
|
|
328
|
+
if (sessionChanged) {
|
|
329
|
+
this.#invalidateSessionCaches();
|
|
330
|
+
this.#closeStaleActiveWindow();
|
|
331
|
+
}
|
|
288
332
|
this.invalidate();
|
|
289
333
|
}
|
|
290
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
|
+
|
|
291
352
|
updateSettings(settings: StatusLineSettings): void {
|
|
292
353
|
this.#settings = settings;
|
|
293
354
|
this.#effectiveSettings = undefined;
|
|
@@ -306,19 +367,89 @@ export class StatusLineComponent implements Component {
|
|
|
306
367
|
this.#subagentCount = count;
|
|
307
368
|
}
|
|
308
369
|
|
|
309
|
-
/** Hub key label shown in the forced running-subagents badge. */
|
|
310
|
-
setSubagentHubHint(hint: string | undefined): void {
|
|
311
|
-
const trimmed = hint?.trim();
|
|
312
|
-
this.#subagentHubHint = trimmed ? trimmed : undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
370
|
/** Active subagent count as currently displayed (collab state mirroring). */
|
|
316
371
|
get subagentCount(): number {
|
|
317
372
|
return this.#subagentCount;
|
|
318
373
|
}
|
|
319
374
|
|
|
320
|
-
|
|
321
|
-
|
|
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;
|
|
322
453
|
}
|
|
323
454
|
|
|
324
455
|
setPlanModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
|
|
@@ -861,6 +992,7 @@ export class StatusLineComponent implements Component {
|
|
|
861
992
|
activeRepo: activeRepoCache.activeRepo,
|
|
862
993
|
width,
|
|
863
994
|
options: segmentOptions ?? {},
|
|
995
|
+
compactThinkingLevel: this.#resolveSettings().compactThinkingLevel ?? false,
|
|
864
996
|
planMode: this.#planModeStatus,
|
|
865
997
|
loopMode: this.#loopModeStatus,
|
|
866
998
|
goalMode: this.#goalModeStatus,
|
|
@@ -871,7 +1003,7 @@ export class StatusLineComponent implements Component {
|
|
|
871
1003
|
contextWindow,
|
|
872
1004
|
autoCompactEnabled: this.#autoCompactEnabled,
|
|
873
1005
|
subagentCount: this.#subagentCount,
|
|
874
|
-
|
|
1006
|
+
activeMs: this.getActiveMs(),
|
|
875
1007
|
git: {
|
|
876
1008
|
branch: gitBranch,
|
|
877
1009
|
status: gitStatus,
|
|
@@ -925,8 +1057,7 @@ export class StatusLineComponent implements Component {
|
|
|
925
1057
|
#subagentBadgeText(): string | undefined {
|
|
926
1058
|
if (this.#subagentCount === 0) return undefined;
|
|
927
1059
|
const noun = this.#subagentCount === 1 ? "agent" : "agents";
|
|
928
|
-
|
|
929
|
-
return theme.fg("statusLineSubagents", `${theme.icon.agents} ${this.#subagentCount} ${noun} running${hubHint}`);
|
|
1060
|
+
return theme.fg("statusLineSubagents", `${theme.icon.agents} ${this.#subagentCount} ${noun}`);
|
|
930
1061
|
}
|
|
931
1062
|
|
|
932
1063
|
#buildStatusLine(width: number): string {
|
|
@@ -20,8 +20,18 @@ 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
|
-
for (const root of [
|
|
34
|
+
for (const root of [path.join(os.homedir(), "Projects"), "/work"]) {
|
|
25
35
|
const relative = relativePathWithinRoot(root, pwd);
|
|
26
36
|
if (relative) return relative;
|
|
27
37
|
}
|
|
@@ -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;
|