@oh-my-pi/pi-coding-agent 16.3.10 → 16.3.12
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 +61 -0
- package/dist/cli.js +3368 -3277
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/model-discovery.ts +18 -2
- package/src/config/model-registry.ts +9 -0
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +11 -0
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.ts +6 -35
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +54 -10
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/title-system.md +10 -10
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +128 -14
- package/src/system-prompt.test.ts +34 -1
- package/src/system-prompt.ts +27 -10
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +53 -14
- package/src/tools/index.ts +11 -0
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +108 -50
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
- package/src/utils/title-generator.ts +40 -65
- package/src/prompts/system/title-system-marker.md +0 -17
|
@@ -20,11 +20,13 @@ export function resetRegisteredArtifactDirsForTests(): void {
|
|
|
20
20
|
/**
|
|
21
21
|
* Snapshot of artifacts dirs for every registered session, deduped.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Collects TWO candidate dirs per ref, because a subagent reads from its
|
|
24
|
+
* adopted (root-wide) `ArtifactManager.dir` but its own children are written
|
|
25
|
+
* one level deeper, under `sessionFile.slice(0, -6)` (`task/index.ts`). A
|
|
26
|
+
* depth-2+ subagent's output therefore lives in the write-time dir, not the
|
|
27
|
+
* adopted one, so `agent://` must scan both or it 404s a live nested peer.
|
|
28
|
+
* `addDir` dedup collapses the depth-0 case (both formulas agree) back to a
|
|
29
|
+
* single entry.
|
|
28
30
|
*/
|
|
29
31
|
export function artifactsDirsFromRegistry(): string[] {
|
|
30
32
|
const dirs: string[] = [];
|
|
@@ -33,7 +35,8 @@ export function artifactsDirsFromRegistry(): string[] {
|
|
|
33
35
|
if (!dirs.includes(dir)) dirs.push(dir);
|
|
34
36
|
};
|
|
35
37
|
for (const ref of AgentRegistry.global().list()) {
|
|
36
|
-
addDir(ref.session?.sessionManager.getArtifactsDir()
|
|
38
|
+
addDir(ref.session?.sessionManager.getArtifactsDir());
|
|
39
|
+
if (ref.sessionFile) addDir(ref.sessionFile.slice(0, -6));
|
|
37
40
|
}
|
|
38
41
|
for (const dir of extraArtifactsDirs) addDir(dir);
|
|
39
42
|
return dirs;
|
|
@@ -90,16 +90,20 @@ interface RoleAssignment {
|
|
|
90
90
|
autoSelected: boolean;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
type ModelSelectorAction = "modelRole" | "retryFallback";
|
|
94
|
+
|
|
93
95
|
type RoleSelectCallback = (
|
|
94
96
|
model: Model,
|
|
95
97
|
role: string | null,
|
|
96
98
|
thinkingLevel?: ConfiguredThinkingLevel,
|
|
97
99
|
selector?: string,
|
|
100
|
+
action?: ModelSelectorAction,
|
|
98
101
|
) => void;
|
|
99
102
|
type CancelCallback = () => void;
|
|
100
103
|
interface MenuRoleAction {
|
|
101
104
|
label: string;
|
|
102
|
-
role: string;
|
|
105
|
+
role: string;
|
|
106
|
+
action: ModelSelectorAction;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
interface ProviderTabState {
|
|
@@ -284,14 +288,19 @@ export class ModelSelectorComponent extends Container {
|
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
#buildMenuRoleActions(): void {
|
|
287
|
-
|
|
291
|
+
const roleActions = getKnownRoleIds(this.#settings).map(role => {
|
|
288
292
|
const roleInfo = getRoleInfo(role, this.#settings);
|
|
289
293
|
const roleLabel = roleInfo.tag ? `${roleInfo.tag} (${roleInfo.name})` : roleInfo.name;
|
|
290
294
|
return {
|
|
291
295
|
label: `Set as ${roleLabel}`,
|
|
292
296
|
role,
|
|
297
|
+
action: "modelRole" as const,
|
|
293
298
|
};
|
|
294
299
|
});
|
|
300
|
+
this.#menuRoleActions = [
|
|
301
|
+
...roleActions,
|
|
302
|
+
{ label: "Set as DEFAULT retry fallback", role: "default", action: "retryFallback" },
|
|
303
|
+
];
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
#loadRoleModels(autoCandidateModels?: ReadonlyArray<Model>): void {
|
|
@@ -1195,6 +1204,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
1195
1204
|
if (this.#menuStep === "role") {
|
|
1196
1205
|
const action = this.#menuRoleActions[this.#menuSelectedIndex];
|
|
1197
1206
|
if (!action) return;
|
|
1207
|
+
if (action.action === "retryFallback") {
|
|
1208
|
+
this.#handleSelect(selectedItem, action.role, undefined, action.action);
|
|
1209
|
+
this.#closeMenu();
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1198
1212
|
this.#menuSelectedRole = action.role;
|
|
1199
1213
|
this.#menuStep = "thinking";
|
|
1200
1214
|
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
@@ -1206,7 +1220,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1206
1220
|
const thinkingOptions = this.#getThinkingLevelsForModel(selectedItem.model);
|
|
1207
1221
|
const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
|
|
1208
1222
|
if (!thinkingLevel) return;
|
|
1209
|
-
this.#handleSelect(selectedItem, this.#menuSelectedRole, thinkingLevel);
|
|
1223
|
+
this.#handleSelect(selectedItem, this.#menuSelectedRole, thinkingLevel, "modelRole");
|
|
1210
1224
|
this.#closeMenu();
|
|
1211
1225
|
return;
|
|
1212
1226
|
}
|
|
@@ -1225,13 +1239,23 @@ export class ModelSelectorComponent extends Container {
|
|
|
1225
1239
|
}
|
|
1226
1240
|
}
|
|
1227
1241
|
|
|
1228
|
-
#handleSelect(
|
|
1242
|
+
#handleSelect(
|
|
1243
|
+
item: ModelItem,
|
|
1244
|
+
role: string | null,
|
|
1245
|
+
thinkingLevel?: ConfiguredThinkingLevel,
|
|
1246
|
+
action: ModelSelectorAction = "modelRole",
|
|
1247
|
+
): void {
|
|
1229
1248
|
if (this.#isItemDisabled(item)) {
|
|
1230
1249
|
return;
|
|
1231
1250
|
}
|
|
1232
1251
|
// For temporary role, don't save to settings - just notify caller
|
|
1233
1252
|
if (role === null) {
|
|
1234
|
-
this.#onSelectCallback(item.model, null, undefined, item.selector);
|
|
1253
|
+
this.#onSelectCallback(item.model, null, undefined, item.selector, action);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (action === "retryFallback") {
|
|
1258
|
+
this.#onSelectCallback(item.model, role, undefined, item.selector, action);
|
|
1235
1259
|
return;
|
|
1236
1260
|
}
|
|
1237
1261
|
|
|
@@ -1241,7 +1265,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1241
1265
|
this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel, autoSelected: false };
|
|
1242
1266
|
|
|
1243
1267
|
// Notify caller (for updating agent state if needed)
|
|
1244
|
-
this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector);
|
|
1268
|
+
this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector, action);
|
|
1245
1269
|
|
|
1246
1270
|
// Update list to show new badges
|
|
1247
1271
|
this.#updateList();
|
|
@@ -180,7 +180,7 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
if (schemaType === "record") {
|
|
183
|
-
return path === "providers.maxInFlightRequests" ? { ...base, type: "providerLimits" } :
|
|
183
|
+
return path === "providers.maxInFlightRequests" ? { ...base, type: "providerLimits" } : { ...base, type: "text" };
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
return null;
|
|
@@ -1235,9 +1235,21 @@ export class StatusLineComponent implements Component {
|
|
|
1235
1235
|
}
|
|
1236
1236
|
}
|
|
1237
1237
|
}
|
|
1238
|
+
const leftOverflowDropIndex = (): number => {
|
|
1239
|
+
// Preserve the current working directory as long as possible. The
|
|
1240
|
+
// previous right-to-left pop could collapse a normal-width bar to
|
|
1241
|
+
// just the model segment, hiding the path before less-critical left
|
|
1242
|
+
// segments such as model/mode/collab were removed.
|
|
1243
|
+
for (let i = leftSegIds.length - 1; i >= 0; i--) {
|
|
1244
|
+
if (leftSegIds[i] !== "path") return i;
|
|
1245
|
+
}
|
|
1246
|
+
return left.length - 1;
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1238
1249
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
1239
|
-
|
|
1240
|
-
|
|
1250
|
+
const dropIdx = leftOverflowDropIndex();
|
|
1251
|
+
left.splice(dropIdx, 1);
|
|
1252
|
+
leftSegIds.splice(dropIdx, 1);
|
|
1241
1253
|
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
1242
1254
|
}
|
|
1243
1255
|
}
|
|
@@ -849,11 +849,7 @@ export class CommandController {
|
|
|
849
849
|
}
|
|
850
850
|
|
|
851
851
|
async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
|
|
852
|
-
|
|
853
|
-
this.ctx.loadingAnimation.stop();
|
|
854
|
-
this.ctx.loadingAnimation = undefined;
|
|
855
|
-
}
|
|
856
|
-
this.ctx.statusContainer.clear();
|
|
852
|
+
this.ctx.clearTransientSessionUi();
|
|
857
853
|
|
|
858
854
|
if (this.ctx.session.isCompacting) {
|
|
859
855
|
this.ctx.session.abortCompaction();
|
|
@@ -867,14 +863,9 @@ export class CommandController {
|
|
|
867
863
|
|
|
868
864
|
this.ctx.statusLine.invalidate();
|
|
869
865
|
this.ctx.statusLine.resetActiveTime();
|
|
870
|
-
this.ctx.ui.requestRender();
|
|
871
866
|
this.ctx.updateEditorBorderColor();
|
|
872
|
-
this.ctx.
|
|
873
|
-
this.ctx.
|
|
874
|
-
this.ctx.compactionQueuedMessages = [];
|
|
875
|
-
this.ctx.streamingComponent = undefined;
|
|
876
|
-
this.ctx.streamingMessage = undefined;
|
|
877
|
-
this.ctx.pendingTools.clear();
|
|
867
|
+
this.ctx.clearTransientSessionUi();
|
|
868
|
+
this.ctx.resetTranscript();
|
|
878
869
|
|
|
879
870
|
this.ctx.present([new Spacer(1), new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 1)]);
|
|
880
871
|
await this.ctx.reloadTodos();
|
|
@@ -914,7 +905,7 @@ export class CommandController {
|
|
|
914
905
|
this.ctx.loadingAnimation.stop();
|
|
915
906
|
this.ctx.loadingAnimation = undefined;
|
|
916
907
|
}
|
|
917
|
-
this.ctx.statusContainer.
|
|
908
|
+
this.ctx.statusContainer.disposeChildren();
|
|
918
909
|
|
|
919
910
|
const success = await this.ctx.session.fork();
|
|
920
911
|
if (!success) {
|
|
@@ -1177,7 +1168,7 @@ export class CommandController {
|
|
|
1177
1168
|
this.ctx.loadingAnimation.stop();
|
|
1178
1169
|
this.ctx.loadingAnimation = undefined;
|
|
1179
1170
|
}
|
|
1180
|
-
this.ctx.statusContainer.
|
|
1171
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1181
1172
|
|
|
1182
1173
|
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
1183
1174
|
const compactingLoader = new Loader(
|
|
@@ -1207,7 +1198,7 @@ export class CommandController {
|
|
|
1207
1198
|
await this.ctx.session.compact(instructions, options);
|
|
1208
1199
|
|
|
1209
1200
|
compactingLoader.stop();
|
|
1210
|
-
this.ctx.statusContainer.
|
|
1201
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1211
1202
|
this.ctx.rebuildChatFromMessages();
|
|
1212
1203
|
|
|
1213
1204
|
this.ctx.statusLine.invalidate();
|
|
@@ -1223,7 +1214,7 @@ export class CommandController {
|
|
|
1223
1214
|
}
|
|
1224
1215
|
} finally {
|
|
1225
1216
|
compactingLoader.stop();
|
|
1226
|
-
this.ctx.statusContainer.
|
|
1217
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1227
1218
|
}
|
|
1228
1219
|
// Run the caller's pre-flush hook (e.g. the plan-approval model transition)
|
|
1229
1220
|
// before queued user input is dispatched, so any turn queued during
|
|
@@ -1252,7 +1243,7 @@ export class CommandController {
|
|
|
1252
1243
|
this.ctx.loadingAnimation.stop();
|
|
1253
1244
|
this.ctx.loadingAnimation = undefined;
|
|
1254
1245
|
}
|
|
1255
|
-
this.ctx.statusContainer.
|
|
1246
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1256
1247
|
|
|
1257
1248
|
const handoffLoader = new Loader(
|
|
1258
1249
|
this.ctx.ui,
|
|
@@ -1273,11 +1264,10 @@ export class CommandController {
|
|
|
1273
1264
|
return;
|
|
1274
1265
|
}
|
|
1275
1266
|
|
|
1276
|
-
// Rebuild chat from the new session (which now contains the handoff document)
|
|
1277
|
-
this.ctx.
|
|
1278
|
-
|
|
1267
|
+
// Rebuild chat from the new session (which now contains the handoff document).
|
|
1268
|
+
this.ctx.clearTransientSessionUi();
|
|
1269
|
+
this.ctx.renderInitialMessages();
|
|
1279
1270
|
this.ctx.statusLine.invalidate();
|
|
1280
|
-
this.ctx.ui.requestRender();
|
|
1281
1271
|
this.ctx.updateEditorBorderColor();
|
|
1282
1272
|
await this.ctx.reloadTodos();
|
|
1283
1273
|
|
|
@@ -1297,9 +1287,9 @@ export class CommandController {
|
|
|
1297
1287
|
}
|
|
1298
1288
|
} finally {
|
|
1299
1289
|
handoffLoader.stop();
|
|
1300
|
-
this.ctx.statusContainer.
|
|
1290
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1301
1291
|
}
|
|
1302
|
-
this.ctx.ui.requestRender();
|
|
1292
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1303
1293
|
}
|
|
1304
1294
|
}
|
|
1305
1295
|
|
|
@@ -379,7 +379,7 @@ export class EventController {
|
|
|
379
379
|
if (this.ctx.retryLoader) {
|
|
380
380
|
this.ctx.retryLoader.stop();
|
|
381
381
|
this.ctx.retryLoader = undefined;
|
|
382
|
-
this.ctx.statusContainer.
|
|
382
|
+
this.ctx.statusContainer.disposeChildren();
|
|
383
383
|
}
|
|
384
384
|
this.#cancelIdleCompaction();
|
|
385
385
|
this.#cancelIdleRecap();
|
|
@@ -1083,7 +1083,7 @@ export class EventController {
|
|
|
1083
1083
|
if (this.ctx.loadingAnimation) {
|
|
1084
1084
|
this.ctx.loadingAnimation.stop();
|
|
1085
1085
|
this.ctx.loadingAnimation = undefined;
|
|
1086
|
-
this.ctx.statusContainer.
|
|
1086
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1087
1087
|
}
|
|
1088
1088
|
if (this.ctx.streamingComponent) {
|
|
1089
1089
|
this.ctx.chatContainer.removeChild(this.ctx.streamingComponent);
|
|
@@ -1125,9 +1125,9 @@ export class EventController {
|
|
|
1125
1125
|
|
|
1126
1126
|
/**
|
|
1127
1127
|
* Tear down the live "Working…" loader: stop its animation timer AND clear the
|
|
1128
|
-
* reference. A transient overlay (auto-compaction / auto-retry)
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1128
|
+
* reference. A transient overlay (auto-compaction / auto-retry) can remove the
|
|
1129
|
+
* loader from the container while leaving `ctx.loadingAnimation` set, so the
|
|
1130
|
+
* resumed turn's `agent_start` →
|
|
1131
1131
|
* `ensureLoadingAnimation()` (guarded by `if (!this.loadingAnimation)`) skipped
|
|
1132
1132
|
* re-adding it and the spinner vanished while the agent kept streaming. Nulling
|
|
1133
1133
|
* the reference here lets the next `agent_start` recreate and re-attach it.
|
|
@@ -1168,7 +1168,7 @@ export class EventController {
|
|
|
1168
1168
|
this.#cancelIdleRecap();
|
|
1169
1169
|
this.#setTerminalProgress(true);
|
|
1170
1170
|
this.#stopWorkingLoader();
|
|
1171
|
-
this.ctx.statusContainer.
|
|
1171
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1172
1172
|
const reasonText =
|
|
1173
1173
|
event.reason === "overflow"
|
|
1174
1174
|
? "Context overflow detected, "
|
|
@@ -1203,7 +1203,7 @@ export class EventController {
|
|
|
1203
1203
|
if (this.ctx.autoCompactionLoader) {
|
|
1204
1204
|
this.ctx.autoCompactionLoader.stop();
|
|
1205
1205
|
this.ctx.autoCompactionLoader = undefined;
|
|
1206
|
-
this.ctx.statusContainer.
|
|
1206
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1207
1207
|
}
|
|
1208
1208
|
const isHandoffAction = event.action === "handoff";
|
|
1209
1209
|
const isShakeAction = event.action === "shake";
|
|
@@ -1245,12 +1245,12 @@ export class EventController {
|
|
|
1245
1245
|
} else if (event.errorMessage) {
|
|
1246
1246
|
this.ctx.showWarning(event.errorMessage);
|
|
1247
1247
|
} else if (isHandoffAction) {
|
|
1248
|
-
this.ctx.
|
|
1248
|
+
this.ctx.clearTransientSessionUi();
|
|
1249
1249
|
this.ctx.lastAssistantUsage = undefined;
|
|
1250
|
-
this.ctx.
|
|
1250
|
+
this.ctx.renderInitialMessages();
|
|
1251
1251
|
this.ctx.statusLine.invalidate();
|
|
1252
|
-
this.ctx.ui.requestRender();
|
|
1253
1252
|
await this.ctx.reloadTodos();
|
|
1253
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1254
1254
|
this.ctx.showStatus("Auto-handoff completed");
|
|
1255
1255
|
} else if (event.skipped) {
|
|
1256
1256
|
// Benign skip: no model selected, no candidate models available, or nothing
|
|
@@ -1268,7 +1268,7 @@ export class EventController {
|
|
|
1268
1268
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1269
1269
|
this.#trackRetrySupersededAssistantComponent(this.#lastAssistantComponent);
|
|
1270
1270
|
this.#stopWorkingLoader();
|
|
1271
|
-
this.ctx.statusContainer.
|
|
1271
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1272
1272
|
if (AIError.is(event.errorId, AIError.Flag.ThinkingLoop)) {
|
|
1273
1273
|
// The retry path drops the failed assistant from runtime context. Do not
|
|
1274
1274
|
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
@@ -1292,7 +1292,7 @@ export class EventController {
|
|
|
1292
1292
|
if (this.ctx.retryLoader) {
|
|
1293
1293
|
this.ctx.retryLoader.stop();
|
|
1294
1294
|
this.ctx.retryLoader = undefined;
|
|
1295
|
-
this.ctx.statusContainer.
|
|
1295
|
+
this.ctx.statusContainer.disposeChildren();
|
|
1296
1296
|
}
|
|
1297
1297
|
if (event.success) {
|
|
1298
1298
|
let appliedRecovered = false;
|
|
@@ -162,18 +162,12 @@ export class ExtensionUiController {
|
|
|
162
162
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
163
163
|
reload: async () => {
|
|
164
164
|
await this.ctx.session.reload();
|
|
165
|
-
this.ctx.chatContainer.clear();
|
|
166
165
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
167
166
|
await this.ctx.reloadTodos();
|
|
168
167
|
this.ctx.showStatus("Reloaded session");
|
|
169
168
|
},
|
|
170
169
|
newSession: async options => {
|
|
171
|
-
|
|
172
|
-
if (this.ctx.loadingAnimation) {
|
|
173
|
-
this.ctx.loadingAnimation.stop();
|
|
174
|
-
this.ctx.loadingAnimation = undefined;
|
|
175
|
-
}
|
|
176
|
-
this.ctx.statusContainer.clear();
|
|
170
|
+
this.ctx.clearTransientSessionUi();
|
|
177
171
|
|
|
178
172
|
// Create new session
|
|
179
173
|
this.clearExtensionTerminalInputListeners();
|
|
@@ -192,15 +186,8 @@ export class ExtensionUiController {
|
|
|
192
186
|
// Reset and update status line
|
|
193
187
|
this.ctx.statusLine.invalidate();
|
|
194
188
|
this.ctx.statusLine.resetActiveTime();
|
|
195
|
-
this.ctx.
|
|
196
|
-
|
|
197
|
-
// Clear UI state
|
|
198
|
-
this.ctx.chatContainer.clear();
|
|
199
|
-
this.ctx.pendingMessagesContainer.clear();
|
|
200
|
-
this.ctx.compactionQueuedMessages = [];
|
|
201
|
-
this.ctx.streamingComponent = undefined;
|
|
202
|
-
this.ctx.streamingMessage = undefined;
|
|
203
|
-
this.ctx.pendingTools.clear();
|
|
189
|
+
this.ctx.clearTransientSessionUi();
|
|
190
|
+
this.ctx.resetTranscript();
|
|
204
191
|
|
|
205
192
|
this.ctx.present([
|
|
206
193
|
new Spacer(1),
|
|
@@ -218,7 +205,6 @@ export class ExtensionUiController {
|
|
|
218
205
|
}
|
|
219
206
|
|
|
220
207
|
// Update UI
|
|
221
|
-
this.ctx.chatContainer.clear();
|
|
222
208
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
223
209
|
await this.ctx.reloadTodos();
|
|
224
210
|
this.ctx.editor.setText(result.selectedText);
|
|
@@ -233,7 +219,6 @@ export class ExtensionUiController {
|
|
|
233
219
|
}
|
|
234
220
|
|
|
235
221
|
// Update UI
|
|
236
|
-
this.ctx.chatContainer.clear();
|
|
237
222
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
238
223
|
await this.ctx.reloadTodos();
|
|
239
224
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
@@ -251,7 +236,6 @@ export class ExtensionUiController {
|
|
|
251
236
|
return { cancelled: true };
|
|
252
237
|
}
|
|
253
238
|
setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
|
|
254
|
-
this.ctx.chatContainer.clear();
|
|
255
239
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
256
240
|
await this.ctx.reloadTodos();
|
|
257
241
|
return { cancelled: false };
|
|
@@ -398,18 +382,12 @@ export class ExtensionUiController {
|
|
|
398
382
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
399
383
|
reload: async () => {
|
|
400
384
|
await this.ctx.session.reload();
|
|
401
|
-
this.ctx.chatContainer.clear();
|
|
402
385
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
403
386
|
await this.ctx.reloadTodos();
|
|
404
387
|
this.ctx.showStatus("Reloaded session");
|
|
405
388
|
},
|
|
406
389
|
newSession: async options => {
|
|
407
|
-
|
|
408
|
-
if (this.ctx.loadingAnimation) {
|
|
409
|
-
this.ctx.loadingAnimation.stop();
|
|
410
|
-
this.ctx.loadingAnimation = undefined;
|
|
411
|
-
}
|
|
412
|
-
this.ctx.statusContainer.clear();
|
|
390
|
+
this.ctx.clearTransientSessionUi();
|
|
413
391
|
|
|
414
392
|
// Create new session
|
|
415
393
|
this.clearExtensionTerminalInputListeners();
|
|
@@ -425,12 +403,8 @@ export class ExtensionUiController {
|
|
|
425
403
|
}
|
|
426
404
|
|
|
427
405
|
// Clear UI state
|
|
428
|
-
this.ctx.
|
|
429
|
-
this.ctx.
|
|
430
|
-
this.ctx.compactionQueuedMessages = [];
|
|
431
|
-
this.ctx.streamingComponent = undefined;
|
|
432
|
-
this.ctx.streamingMessage = undefined;
|
|
433
|
-
this.ctx.pendingTools.clear();
|
|
406
|
+
this.ctx.clearTransientSessionUi();
|
|
407
|
+
this.ctx.resetTranscript();
|
|
434
408
|
|
|
435
409
|
this.ctx.present([
|
|
436
410
|
new Spacer(1),
|
|
@@ -448,7 +422,6 @@ export class ExtensionUiController {
|
|
|
448
422
|
}
|
|
449
423
|
|
|
450
424
|
// Update UI
|
|
451
|
-
this.ctx.chatContainer.clear();
|
|
452
425
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
453
426
|
await this.ctx.reloadTodos();
|
|
454
427
|
this.ctx.editor.setText(result.selectedText);
|
|
@@ -463,7 +436,6 @@ export class ExtensionUiController {
|
|
|
463
436
|
}
|
|
464
437
|
|
|
465
438
|
// Update UI
|
|
466
|
-
this.ctx.chatContainer.clear();
|
|
467
439
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
468
440
|
await this.ctx.reloadTodos();
|
|
469
441
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
@@ -480,7 +452,6 @@ export class ExtensionUiController {
|
|
|
480
452
|
if (!result) {
|
|
481
453
|
return { cancelled: true };
|
|
482
454
|
}
|
|
483
|
-
this.ctx.chatContainer.clear();
|
|
484
455
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
485
456
|
await this.ctx.reloadTodos();
|
|
486
457
|
return { cancelled: false };
|
|
@@ -86,6 +86,20 @@ function hasPasteText(value: unknown): value is PasteTarget {
|
|
|
86
86
|
return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const SHELL_PROMPT_COMMAND_RE =
|
|
90
|
+
/^(?:\.{0,2}\/|~\/|cd(?:\s|$)|sudo(?:\s|$)|git(?:\s|$)|bun(?:\s|$)|npm(?:\s|$)|pnpm(?:\s|$)|yarn(?:\s|$)|node(?:\s|$)|python\d*(?:\s|$)|cargo(?:\s|$)|go(?:\s|$)|make(?:\s|$)|docker(?:\s|$)|kubectl(?:\s|$))/;
|
|
91
|
+
const SHELL_PROMPT_OPERATOR_RE = /(?:^|\s)(?:&&|\|\||\||2>&1|[<>]{1,2})(?:\s|$)/;
|
|
92
|
+
const OMP_STATUS_LINE_RE = /^\s*in:\s+\d+\s+out:\s+\d+(?:\s+cache\s+\S+)?\s+t:\s+\S+\s+tok\/s:\s+\S+/m;
|
|
93
|
+
|
|
94
|
+
function looksLikePastedShellPrompt(code: string): boolean {
|
|
95
|
+
const firstLine = code.split("\n", 1)[0]?.trimStart() ?? "";
|
|
96
|
+
return (
|
|
97
|
+
SHELL_PROMPT_COMMAND_RE.test(firstLine) ||
|
|
98
|
+
SHELL_PROMPT_OPERATOR_RE.test(firstLine) ||
|
|
99
|
+
OMP_STATUS_LINE_RE.test(code)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
function pythonCommandPrefixLength(trimmedText: string): 0 | 1 | 2 {
|
|
90
104
|
if (trimmedText.charCodeAt(0) !== 36 /* $ */) return 0;
|
|
91
105
|
if (trimmedText.charCodeAt(1) === 123 /* { */) return 0;
|
|
@@ -100,8 +114,10 @@ function parsePythonCommandInput(text: string): { code: string; isExcluded: bool
|
|
|
100
114
|
const trimmed = text.trimStart();
|
|
101
115
|
const prefixLength = pythonCommandPrefixLength(trimmed);
|
|
102
116
|
if (prefixLength === 0) return undefined;
|
|
117
|
+
const code = trimmed.slice(prefixLength).trim();
|
|
118
|
+
if (prefixLength === 1 && looksLikePastedShellPrompt(code)) return undefined;
|
|
103
119
|
return {
|
|
104
|
-
code
|
|
120
|
+
code,
|
|
105
121
|
isExcluded: prefixLength === 2,
|
|
106
122
|
};
|
|
107
123
|
}
|
|
@@ -536,7 +552,7 @@ export class InputController {
|
|
|
536
552
|
const wasPythonMode = this.ctx.isPythonMode;
|
|
537
553
|
const trimmed = text.trimStart();
|
|
538
554
|
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
539
|
-
this.ctx.isPythonMode =
|
|
555
|
+
this.ctx.isPythonMode = parsePythonCommandInput(trimmed) !== undefined;
|
|
540
556
|
if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
|
|
541
557
|
this.ctx.updateEditorBorderColor();
|
|
542
558
|
}
|
|
@@ -88,12 +88,14 @@ function raceAbortSignal<T>(promise: Promise<T>, signal: AbortSignal, createErro
|
|
|
88
88
|
const MCP_AUTH_MIN_WRAP_WIDTH = 16;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Wrap `url` into rows that each fit inside `width
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
91
|
+
* Wrap `url` into rows that each fit inside `width`. When the label + URL fit
|
|
92
|
+
* on one line, returns a single indented row; otherwise puts the label on its
|
|
93
|
+
* own indented row and slices the URL into fixed-width chunks that start at
|
|
94
|
+
* column 0. Continuation chunks carry ZERO leading bytes on purpose: a
|
|
95
|
+
* multi-row terminal selection includes the newline plus any leading indent,
|
|
96
|
+
* and while address bars strip newlines they preserve or percent-encode
|
|
97
|
+
* embedded spaces — an indent would corrupt the URL at every chunk boundary
|
|
98
|
+
* (silently, when the damage lands inside a query value).
|
|
97
99
|
*/
|
|
98
100
|
function wrapUrlRows(label: string, url: string, width: number): string[] {
|
|
99
101
|
const indent = " ";
|
|
@@ -103,10 +105,9 @@ function wrapUrlRows(label: string, url: string, width: number): string[] {
|
|
|
103
105
|
if (inlineWidth <= effective) {
|
|
104
106
|
return [`${indent}${theme.fg("muted", `${label} ${sanitized}`)}`];
|
|
105
107
|
}
|
|
106
|
-
const chunkWidth = Math.max(1, effective - indent.length);
|
|
107
108
|
const rows: string[] = [`${indent}${theme.fg("muted", label)}`];
|
|
108
|
-
for (let i = 0; i < sanitized.length; i +=
|
|
109
|
-
rows.push(
|
|
109
|
+
for (let i = 0; i < sanitized.length; i += effective) {
|
|
110
|
+
rows.push(theme.fg("muted", sanitized.slice(i, i + effective)));
|
|
110
111
|
}
|
|
111
112
|
return rows;
|
|
112
113
|
}
|
|
@@ -593,13 +593,27 @@ export class SelectorController {
|
|
|
593
593
|
this.ctx.settings,
|
|
594
594
|
this.ctx.session.modelRegistry,
|
|
595
595
|
this.ctx.session.scopedModels,
|
|
596
|
-
async (model, role, thinkingLevel, selector) => {
|
|
596
|
+
async (model, role, thinkingLevel, selector, action) => {
|
|
597
597
|
// `auto` is session-global: never baked into a per-role model value
|
|
598
598
|
// (it can't round-trip through `model:<level>`). Apply it to the session
|
|
599
599
|
// separately and persist via `defaultThinkingLevel`.
|
|
600
600
|
const isAuto = thinkingLevel === AUTO_THINKING;
|
|
601
601
|
const concreteThinking = isAuto ? undefined : thinkingLevel;
|
|
602
|
+
const selectorValue = selector ?? `${model.provider}/${model.id}`;
|
|
602
603
|
try {
|
|
604
|
+
if (action === "retryFallback" && role !== null) {
|
|
605
|
+
const fallbackSelector = formatModelSelectorValue(selectorValue, concreteThinking);
|
|
606
|
+
const fallbackChains = this.ctx.settings.get("retry.fallbackChains");
|
|
607
|
+
const chain = Array.isArray(fallbackChains[role]) ? fallbackChains[role] : [];
|
|
608
|
+
this.ctx.settings.set("retry.fallbackChains", {
|
|
609
|
+
...fallbackChains,
|
|
610
|
+
[role]: [fallbackSelector, ...chain.filter(existing => existing !== fallbackSelector)],
|
|
611
|
+
});
|
|
612
|
+
const roleInfo = getRoleInfo(role, settings);
|
|
613
|
+
const roleLabel = roleInfo?.name ?? role;
|
|
614
|
+
this.ctx.showStatus(`${roleLabel} fallback model: ${fallbackSelector}`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
603
617
|
if (role === null) {
|
|
604
618
|
// Temporary: update agent state but don't persist the model to settings
|
|
605
619
|
await this.ctx.session.setModelTemporary(model);
|
|
@@ -771,7 +785,6 @@ export class SelectorController {
|
|
|
771
785
|
return;
|
|
772
786
|
}
|
|
773
787
|
|
|
774
|
-
this.ctx.chatContainer.clear();
|
|
775
788
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
776
789
|
this.ctx.editor.setText(result.selectedText);
|
|
777
790
|
done();
|
|
@@ -915,7 +928,6 @@ export class SelectorController {
|
|
|
915
928
|
|
|
916
929
|
// Update UI — rebuild the display transcript for the new leaf (the
|
|
917
930
|
// context from navigateTree is the LLM context, not the transcript).
|
|
918
|
-
this.ctx.chatContainer.clear();
|
|
919
931
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
920
932
|
await this.ctx.reloadTodos();
|
|
921
933
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
@@ -927,7 +939,7 @@ export class SelectorController {
|
|
|
927
939
|
} finally {
|
|
928
940
|
if (summaryLoader) {
|
|
929
941
|
summaryLoader.stop();
|
|
930
|
-
this.ctx.statusContainer.
|
|
942
|
+
this.ctx.statusContainer.disposeChildren();
|
|
931
943
|
}
|
|
932
944
|
this.ctx.editor.onEscape = originalOnEscape;
|
|
933
945
|
}
|
|
@@ -1067,7 +1079,6 @@ export class SelectorController {
|
|
|
1067
1079
|
this.ctx.updateEditorBorderColor();
|
|
1068
1080
|
|
|
1069
1081
|
// Clear and re-render the chat
|
|
1070
|
-
this.ctx.chatContainer.clear();
|
|
1071
1082
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
1072
1083
|
await this.ctx.reloadTodos();
|
|
1073
1084
|
this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
|