@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.13
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 +65 -0
- package/dist/cli.js +3176 -3087
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/keybindings.d.ts +9 -4
- 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 +3 -1
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- 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/mnemopi/state.d.ts +7 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +1 -0
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- 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 +8 -4
- 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 +3 -0
- package/dist/types/tools/renderers.d.ts +12 -5
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tools/write.d.ts +1 -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/keybindings.ts +62 -10
- package/src/config/model-registry.ts +94 -20
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +59 -21
- 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/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- 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/mnemopi/state.ts +19 -5
- package/src/modes/acp/acp-agent.ts +69 -8
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/read-tool-group.ts +5 -1
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/components/tool-execution.ts +28 -24
- 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.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -35
- package/src/modes/controllers/input-controller.ts +23 -57
- 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 +97 -12
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/rpc/rpc-client.ts +42 -13
- package/src/modes/rpc/rpc-mode.ts +21 -19
- package/src/modes/types.ts +3 -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/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +136 -19
- package/src/system-prompt.ts +3 -2
- 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 +105 -21
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +11 -0
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +135 -57
- package/src/tools/renderers.ts +13 -5
- package/src/tools/ssh.ts +10 -3
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +26 -0
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
|
@@ -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;
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -108,11 +108,16 @@ export interface MnemopiMemoryEditOptions {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export interface MnemopiMemoryEditResult {
|
|
111
|
-
status: "updated" | "deleted" | "invalidated" | "not_found";
|
|
111
|
+
status: "updated" | "deleted" | "invalidated" | "not_found" | "not_editable";
|
|
112
112
|
bank?: string;
|
|
113
|
-
store?:
|
|
113
|
+
store?: MnemopiMemoryStore;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Which mnemopi table a resolved memory id lives in. `fact` rows are
|
|
117
|
+
* read-only projections of fact extraction (issue #4725): resolvable for
|
|
118
|
+
* reads, never editable. */
|
|
119
|
+
export type MnemopiMemoryStore = "working" | "episodic" | "fact";
|
|
120
|
+
|
|
116
121
|
interface MnemopiStoredMemoryRow {
|
|
117
122
|
id?: unknown;
|
|
118
123
|
content?: unknown;
|
|
@@ -136,7 +141,7 @@ interface MnemopiStoredMemoryRow {
|
|
|
136
141
|
*/
|
|
137
142
|
export interface MnemopiScopedMemoryHit {
|
|
138
143
|
bank: string;
|
|
139
|
-
store:
|
|
144
|
+
store: MnemopiMemoryStore;
|
|
140
145
|
row: {
|
|
141
146
|
id: string;
|
|
142
147
|
content: string;
|
|
@@ -256,7 +261,8 @@ export class MnemopiSessionState {
|
|
|
256
261
|
for (const target of targets) {
|
|
257
262
|
const raw = target.memory.get(id) as MnemopiStoredMemoryRow | null;
|
|
258
263
|
if (!raw) continue;
|
|
259
|
-
const store:
|
|
264
|
+
const store: MnemopiMemoryStore =
|
|
265
|
+
raw.memory_store === "episodic" || raw.memory_store === "fact" ? raw.memory_store : "working";
|
|
260
266
|
return {
|
|
261
267
|
bank: target.bank,
|
|
262
268
|
store,
|
|
@@ -291,8 +297,16 @@ export class MnemopiSessionState {
|
|
|
291
297
|
for (const target of targets) {
|
|
292
298
|
const row = target.memory.get(id) as MnemopiStoredMemoryRow | null;
|
|
293
299
|
if (!row) continue;
|
|
294
|
-
const store:
|
|
300
|
+
const store: MnemopiMemoryStore =
|
|
301
|
+
row.memory_store === "episodic" || row.memory_store === "fact" ? row.memory_store : "working";
|
|
295
302
|
const resultContext: Pick<MnemopiMemoryEditResult, "bank" | "store"> = { bank: target.bank, store };
|
|
303
|
+
if (store === "fact") {
|
|
304
|
+
// Facts are read-only: no memory_edit op mutates the facts
|
|
305
|
+
// table, so report that precisely instead of `not_found`
|
|
306
|
+
// (the id DID resolve — issue #4725).
|
|
307
|
+
ineligible ??= { status: "not_editable", ...resultContext };
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
296
310
|
if ((op === "update" || op === "forget") && store !== "working") {
|
|
297
311
|
ineligible ??= { status: "not_found", ...resultContext };
|
|
298
312
|
continue;
|
|
@@ -84,6 +84,7 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
|
84
84
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
85
85
|
import {
|
|
86
86
|
buildToolCallStartUpdate,
|
|
87
|
+
extractAssistantMessageText,
|
|
87
88
|
mapAgentSessionEventToAcpSessionUpdates,
|
|
88
89
|
normalizeReplayToolArguments,
|
|
89
90
|
} from "./acp-event-mapper";
|
|
@@ -425,6 +426,7 @@ export function createAcpExtensionUiContext(
|
|
|
425
426
|
setEditorText: () => {},
|
|
426
427
|
getEditorText: () => "",
|
|
427
428
|
editor: async () => undefined,
|
|
429
|
+
addAutocompleteProvider: () => {},
|
|
428
430
|
setEditorComponent: () => {},
|
|
429
431
|
get theme() {
|
|
430
432
|
return theme;
|
|
@@ -843,13 +845,16 @@ export class AcpAgent implements Agent {
|
|
|
843
845
|
return false;
|
|
844
846
|
}
|
|
845
847
|
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
|
846
|
-
await record.session.promptCustomMessage(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
848
|
+
await record.session.promptCustomMessage(
|
|
849
|
+
{
|
|
850
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
851
|
+
content: built.message,
|
|
852
|
+
display: true,
|
|
853
|
+
details: built.details,
|
|
854
|
+
attribution: "user",
|
|
855
|
+
},
|
|
856
|
+
{ streamingBehavior: "steer" },
|
|
857
|
+
);
|
|
853
858
|
return true;
|
|
854
859
|
}
|
|
855
860
|
|
|
@@ -1210,8 +1215,11 @@ export class AcpAgent implements Agent {
|
|
|
1210
1215
|
this.#clearLiveAssistantMessageAfterEvent(record, event);
|
|
1211
1216
|
|
|
1212
1217
|
if (event.type === "agent_end") {
|
|
1218
|
+
await this.#flushMissedFinalAssistantText(record, event);
|
|
1213
1219
|
await this.#emitEndOfTurnUpdates(record);
|
|
1214
1220
|
await this.#waitForAcpPromptIdle(record);
|
|
1221
|
+
record.liveMessageId = undefined;
|
|
1222
|
+
record.liveMessageProgress = undefined;
|
|
1215
1223
|
this.#finishPrompt(record, {
|
|
1216
1224
|
stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
|
|
1217
1225
|
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
@@ -1219,6 +1227,51 @@ export class AcpAgent implements Agent {
|
|
|
1219
1227
|
}
|
|
1220
1228
|
}
|
|
1221
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* Deliver the final visible answer when the assistant `message_end` never
|
|
1232
|
+
* reached this prompt turn's subscription. Session event handlers are
|
|
1233
|
+
* fire-and-forget (`Agent#emit` does not await async listeners), and
|
|
1234
|
+
* `agent_end` is flushed through the session's `#endInFlight` path while the
|
|
1235
|
+
* assistant `message_end` fan-out can still be parked on extension delivery —
|
|
1236
|
+
* so `agent_end` can overtake `message_end`. Once the turn finishes,
|
|
1237
|
+
* `#finishPrompt` unsubscribes and the fallback text emission in
|
|
1238
|
+
* `mapAssistantMessageEnd` is lost for good: a client that only received
|
|
1239
|
+
* `agent_thought_chunk`s stays stuck on the thinking block (#4902). The live
|
|
1240
|
+
* message progress records whether visible text ever reached the client; if
|
|
1241
|
+
* it has not, emit the last assistant message's text before the prompt
|
|
1242
|
+
* resolves. A `message_end` that lands during the end-of-turn waits still
|
|
1243
|
+
* takes the normal mapper path and sees `textEmitted` already set, so the
|
|
1244
|
+
* answer is delivered exactly once.
|
|
1245
|
+
*/
|
|
1246
|
+
async #flushMissedFinalAssistantText(
|
|
1247
|
+
record: ManagedSessionRecord,
|
|
1248
|
+
event: Extract<AgentSessionEvent, { type: "agent_end" }>,
|
|
1249
|
+
): Promise<void> {
|
|
1250
|
+
const progress = record.liveMessageProgress;
|
|
1251
|
+
if (!progress || progress.textEmitted) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const lastAssistant = [...event.messages]
|
|
1255
|
+
.reverse()
|
|
1256
|
+
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
1257
|
+
if (!lastAssistant) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const text = extractAssistantMessageText(lastAssistant);
|
|
1261
|
+
if (text.length === 0) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
progress.textEmitted = true;
|
|
1265
|
+
await this.#connection.sessionUpdate({
|
|
1266
|
+
sessionId: record.session.sessionId,
|
|
1267
|
+
update: {
|
|
1268
|
+
sessionUpdate: "agent_message_chunk",
|
|
1269
|
+
content: { type: "text", text },
|
|
1270
|
+
messageId: record.liveMessageId,
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1222
1275
|
async #waitForAcpPromptIdle(record: ManagedSessionRecord): Promise<void> {
|
|
1223
1276
|
for (let pass = 0; pass < ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES; pass++) {
|
|
1224
1277
|
await record.session.waitForIdle();
|
|
@@ -1244,8 +1297,16 @@ export class AcpAgent implements Agent {
|
|
|
1244
1297
|
}
|
|
1245
1298
|
}
|
|
1246
1299
|
|
|
1300
|
+
/**
|
|
1301
|
+
* Reset live-message tracking once the assistant `message_end` is handled.
|
|
1302
|
+
* The `agent_end` reset happens inside the `agent_end` branch of
|
|
1303
|
+
* `#handlePromptEvent` — after `#flushMissedFinalAssistantText` — so a
|
|
1304
|
+
* `message_end` that arrives during the end-of-turn waits maps against the
|
|
1305
|
+
* real progress instead of resurrecting a fresh one (which would double-emit
|
|
1306
|
+
* the final answer).
|
|
1307
|
+
*/
|
|
1247
1308
|
#clearLiveAssistantMessageAfterEvent(record: ManagedSessionRecord, event: AgentSessionEvent): void {
|
|
1248
|
-
if (
|
|
1309
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
1249
1310
|
record.liveMessageId = undefined;
|
|
1250
1311
|
record.liveMessageProgress = undefined;
|
|
1251
1312
|
}
|
|
@@ -922,7 +922,7 @@ function isTerminalOnlyDetails(value: unknown): boolean {
|
|
|
922
922
|
return content === undefined || (Array.isArray(content) && content.length === 0);
|
|
923
923
|
}
|
|
924
924
|
|
|
925
|
-
function extractAssistantMessageText(value: unknown): string {
|
|
925
|
+
export function extractAssistantMessageText(value: unknown): string {
|
|
926
926
|
if (typeof value !== "object" || value === null || !("content" in value)) {
|
|
927
927
|
return "";
|
|
928
928
|
}
|
|
@@ -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();
|
|
@@ -37,6 +37,7 @@ export function readArgsTargetInternalUrl(args: unknown): boolean {
|
|
|
37
37
|
type ReadRenderArgs = {
|
|
38
38
|
path?: string;
|
|
39
39
|
file_path?: string;
|
|
40
|
+
selector?: string;
|
|
40
41
|
// Legacy field from the old schema; tolerated for rebuilt transcripts.
|
|
41
42
|
sel?: string;
|
|
42
43
|
};
|
|
@@ -344,7 +345,10 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
344
345
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
345
346
|
if (!toolCallId) return;
|
|
346
347
|
const basePath = args.file_path || args.path || "";
|
|
347
|
-
const
|
|
348
|
+
const rawSelector =
|
|
349
|
+
typeof args.selector === "string" ? args.selector : typeof args.sel === "string" ? args.sel : undefined;
|
|
350
|
+
const selector = rawSelector?.trim().replace(/^:+/, "");
|
|
351
|
+
const rawPath = selector && selector.length > 0 ? `${basePath}:${selector}` : basePath;
|
|
348
352
|
const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
|
|
349
353
|
toolCallId,
|
|
350
354
|
path: rawPath,
|
|
@@ -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
|
}
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
resolveImageOptions,
|
|
39
39
|
truncateToWidth,
|
|
40
40
|
} from "../../tools/render-utils";
|
|
41
|
-
import { toolRenderers } from "../../tools/renderers";
|
|
41
|
+
import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
|
|
42
42
|
import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
|
|
43
43
|
import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
|
|
44
44
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
@@ -283,13 +283,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
283
283
|
// history, so progress renders static gray and further partial snapshots are
|
|
284
284
|
// dropped (see #maybeFreezeBackgroundTask).
|
|
285
285
|
#backgroundTaskFrozen = false;
|
|
286
|
-
// Set on each `render()` when the last painted shape
|
|
287
|
-
//
|
|
288
|
-
// so a topology-changing update that lands before the shape reaches
|
|
289
|
-
// terminal never triggers a full-viewport replay (which on direct
|
|
290
|
-
// wipes native scrollback and flashes the user's history —
|
|
291
|
-
// PR #4315).
|
|
292
|
-
#
|
|
286
|
+
// Set on each `render()` when the last painted pending shape must be
|
|
287
|
+
// replayed wholesale when the first result arrives. Reset gates key off
|
|
288
|
+
// these so a topology-changing update that lands before the shape reaches
|
|
289
|
+
// the terminal never triggers a full-viewport replay (which on direct
|
|
290
|
+
// terminals wipes native scrollback and flashes the user's history —
|
|
291
|
+
// reviewer note on PR #4315).
|
|
292
|
+
#firstResultViewportRepaintShapePainted = false;
|
|
293
293
|
#partialResultShapePainted = false;
|
|
294
294
|
#renderState: {
|
|
295
295
|
spinnerFrame?: number;
|
|
@@ -497,9 +497,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
497
497
|
}
|
|
498
498
|
const hadNoResult = this.#result === undefined;
|
|
499
499
|
const wasPartialResult = this.#result !== undefined && this.#isPartial;
|
|
500
|
-
const
|
|
500
|
+
const firstResultRepaintShapePainted = this.#firstResultViewportRepaintShapePainted;
|
|
501
501
|
const partialResultPainted = this.#partialResultShapePainted;
|
|
502
|
-
this.#
|
|
502
|
+
this.#firstResultViewportRepaintShapePainted = false;
|
|
503
503
|
this.#partialResultShapePainted = false;
|
|
504
504
|
this.#result = result;
|
|
505
505
|
this.#resultVersion++;
|
|
@@ -513,7 +513,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
513
513
|
this.#updateTodoStrikeAnimation();
|
|
514
514
|
this.#updateDisplay();
|
|
515
515
|
this.#resetDisplayForResultTopologyChange(
|
|
516
|
-
hadNoResult &&
|
|
516
|
+
hadNoResult && firstResultRepaintShapePainted,
|
|
517
517
|
wasPartialResult && partialResultPainted,
|
|
518
518
|
isPartial,
|
|
519
519
|
);
|
|
@@ -810,34 +810,38 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
810
810
|
this.#displayBuilt = true;
|
|
811
811
|
}
|
|
812
812
|
|
|
813
|
-
#rendererFlag(name: "
|
|
813
|
+
#rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
|
|
814
814
|
const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
|
|
815
815
|
const rendererValue = toolRenderers[this.#toolName]?.[name];
|
|
816
816
|
return toolValue === true || (toolValue === undefined && rendererValue === true);
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
/**
|
|
820
|
-
* True while the last painted shape
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
820
|
+
* True while the last painted pending-call shape opted into a full viewport
|
|
821
|
+
* repaint at the first result (`forceFirstResultViewportRepaint`) — e.g. the
|
|
822
|
+
* streamed SSH placeholder (`⏳ SSH: […]` / `$ …`) or a collapsed write tail
|
|
823
|
+
* window, both of which the first result render re-anchors instead of
|
|
824
|
+
* preserving. Kept as a per-paint fact so a topology-changing update that
|
|
825
|
+
* lands before the pending rows reach the terminal skips the reset.
|
|
824
826
|
*/
|
|
825
|
-
#
|
|
827
|
+
#needsFirstResultViewportRepaintAtRender(): boolean {
|
|
826
828
|
if (this.#result !== undefined) return false;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
+
const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
|
|
830
|
+
?.forceFirstResultViewportRepaint;
|
|
831
|
+
const value =
|
|
832
|
+
toolValue !== undefined ? toolValue : toolRenderers[this.#toolName]?.forceFirstResultViewportRepaint;
|
|
833
|
+
if (typeof value === "function") return value(this.#args, this.#renderState);
|
|
834
|
+
return value === true;
|
|
829
835
|
}
|
|
830
836
|
|
|
831
837
|
#resetDisplayForResultTopologyChange(
|
|
832
|
-
|
|
838
|
+
firstResultAfterRepaintShapePaint: boolean,
|
|
833
839
|
partialResultPaintedBeforeSettle: boolean,
|
|
834
840
|
isPartial: boolean,
|
|
835
841
|
): void {
|
|
836
|
-
const firstResultReplacesStreamedPlaceholder =
|
|
837
|
-
firstResultAfterPlaceholderPaint && this.#rendererFlag("forceFirstResultViewportRepaint");
|
|
838
842
|
const provisionalResultSettled =
|
|
839
843
|
partialResultPaintedBeforeSettle && !isPartial && this.#rendererFlag("forceResultViewportRepaintOnSettle");
|
|
840
|
-
if (
|
|
844
|
+
if (firstResultAfterRepaintShapePaint || provisionalResultSettled) {
|
|
841
845
|
this.#ui.resetDisplay();
|
|
842
846
|
}
|
|
843
847
|
}
|
|
@@ -848,7 +852,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
848
852
|
// override runs on every compose the parent Container performs, so a
|
|
849
853
|
// frame that never gets composed leaves the flags false and prevents a
|
|
850
854
|
// spurious `resetDisplay()`.
|
|
851
|
-
this.#
|
|
855
|
+
this.#firstResultViewportRepaintShapePainted = this.#needsFirstResultViewportRepaintAtRender();
|
|
852
856
|
this.#partialResultShapePainted = this.#result !== undefined && this.#isPartial;
|
|
853
857
|
return lines;
|
|
854
858
|
}
|
|
@@ -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;
|