@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
|
@@ -8,6 +8,7 @@ import { ExtensionUiController } from "./extension-ui-controller";
|
|
|
8
8
|
function makeHarness() {
|
|
9
9
|
const editor = new CustomEditor(getEditorTheme());
|
|
10
10
|
const requestRender = vi.fn();
|
|
11
|
+
const addAutocompleteProvider = vi.fn();
|
|
11
12
|
let uiContext: ExtensionUIContext | undefined;
|
|
12
13
|
const ctx = {
|
|
13
14
|
editor,
|
|
@@ -21,11 +22,13 @@ function makeHarness() {
|
|
|
21
22
|
expect(hasUI).toBe(true);
|
|
22
23
|
uiContext = context;
|
|
23
24
|
},
|
|
25
|
+
addAutocompleteProvider,
|
|
24
26
|
} as unknown as InteractiveModeContext;
|
|
25
27
|
|
|
26
28
|
return {
|
|
27
29
|
editor,
|
|
28
30
|
requestRender,
|
|
31
|
+
addAutocompleteProvider,
|
|
29
32
|
async init(): Promise<ExtensionUIContext> {
|
|
30
33
|
await new ExtensionUiController(ctx).initHooksAndCustomTools();
|
|
31
34
|
expect(uiContext).toBeDefined();
|
|
@@ -55,4 +58,17 @@ describe("ExtensionUiController editor UI", () => {
|
|
|
55
58
|
expect(harness.editor.getText()).toBe("hello");
|
|
56
59
|
expect(harness.requestRender).toHaveBeenCalledTimes(1);
|
|
57
60
|
});
|
|
61
|
+
|
|
62
|
+
it("bridges addAutocompleteProvider factories to the interactive mode context (#4919)", async () => {
|
|
63
|
+
const harness = makeHarness();
|
|
64
|
+
const ui = await harness.init();
|
|
65
|
+
|
|
66
|
+
expect(typeof ui.addAutocompleteProvider).toBe("function");
|
|
67
|
+
|
|
68
|
+
const factory = (current: unknown) => current as never;
|
|
69
|
+
ui.addAutocompleteProvider(factory);
|
|
70
|
+
|
|
71
|
+
expect(harness.addAutocompleteProvider).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(harness.addAutocompleteProvider).toHaveBeenCalledWith(factory);
|
|
73
|
+
});
|
|
58
74
|
});
|
|
@@ -82,6 +82,7 @@ export class ExtensionUiController {
|
|
|
82
82
|
getEditorText: () => this.ctx.editor.getText(),
|
|
83
83
|
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
84
84
|
this.showCollabAwareEditor(title, prefill, dialogOptions, editorOptions),
|
|
85
|
+
addAutocompleteProvider: factory => this.ctx.addAutocompleteProvider(factory),
|
|
85
86
|
get theme() {
|
|
86
87
|
return theme;
|
|
87
88
|
},
|
|
@@ -162,18 +163,12 @@ export class ExtensionUiController {
|
|
|
162
163
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
163
164
|
reload: async () => {
|
|
164
165
|
await this.ctx.session.reload();
|
|
165
|
-
this.ctx.chatContainer.clear();
|
|
166
166
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
167
167
|
await this.ctx.reloadTodos();
|
|
168
168
|
this.ctx.showStatus("Reloaded session");
|
|
169
169
|
},
|
|
170
170
|
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();
|
|
171
|
+
this.ctx.clearTransientSessionUi();
|
|
177
172
|
|
|
178
173
|
// Create new session
|
|
179
174
|
this.clearExtensionTerminalInputListeners();
|
|
@@ -192,15 +187,8 @@ export class ExtensionUiController {
|
|
|
192
187
|
// Reset and update status line
|
|
193
188
|
this.ctx.statusLine.invalidate();
|
|
194
189
|
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();
|
|
190
|
+
this.ctx.clearTransientSessionUi();
|
|
191
|
+
this.ctx.resetTranscript();
|
|
204
192
|
|
|
205
193
|
this.ctx.present([
|
|
206
194
|
new Spacer(1),
|
|
@@ -218,7 +206,6 @@ export class ExtensionUiController {
|
|
|
218
206
|
}
|
|
219
207
|
|
|
220
208
|
// Update UI
|
|
221
|
-
this.ctx.chatContainer.clear();
|
|
222
209
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
223
210
|
await this.ctx.reloadTodos();
|
|
224
211
|
this.ctx.editor.setText(result.selectedText);
|
|
@@ -233,7 +220,6 @@ export class ExtensionUiController {
|
|
|
233
220
|
}
|
|
234
221
|
|
|
235
222
|
// Update UI
|
|
236
|
-
this.ctx.chatContainer.clear();
|
|
237
223
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
238
224
|
await this.ctx.reloadTodos();
|
|
239
225
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
@@ -251,7 +237,6 @@ export class ExtensionUiController {
|
|
|
251
237
|
return { cancelled: true };
|
|
252
238
|
}
|
|
253
239
|
setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
|
|
254
|
-
this.ctx.chatContainer.clear();
|
|
255
240
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
256
241
|
await this.ctx.reloadTodos();
|
|
257
242
|
return { cancelled: false };
|
|
@@ -398,18 +383,12 @@ export class ExtensionUiController {
|
|
|
398
383
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
399
384
|
reload: async () => {
|
|
400
385
|
await this.ctx.session.reload();
|
|
401
|
-
this.ctx.chatContainer.clear();
|
|
402
386
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
403
387
|
await this.ctx.reloadTodos();
|
|
404
388
|
this.ctx.showStatus("Reloaded session");
|
|
405
389
|
},
|
|
406
390
|
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();
|
|
391
|
+
this.ctx.clearTransientSessionUi();
|
|
413
392
|
|
|
414
393
|
// Create new session
|
|
415
394
|
this.clearExtensionTerminalInputListeners();
|
|
@@ -425,12 +404,8 @@ export class ExtensionUiController {
|
|
|
425
404
|
}
|
|
426
405
|
|
|
427
406
|
// 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();
|
|
407
|
+
this.ctx.clearTransientSessionUi();
|
|
408
|
+
this.ctx.resetTranscript();
|
|
434
409
|
|
|
435
410
|
this.ctx.present([
|
|
436
411
|
new Spacer(1),
|
|
@@ -448,7 +423,6 @@ export class ExtensionUiController {
|
|
|
448
423
|
}
|
|
449
424
|
|
|
450
425
|
// Update UI
|
|
451
|
-
this.ctx.chatContainer.clear();
|
|
452
426
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
453
427
|
await this.ctx.reloadTodos();
|
|
454
428
|
this.ctx.editor.setText(result.selectedText);
|
|
@@ -463,7 +437,6 @@ export class ExtensionUiController {
|
|
|
463
437
|
}
|
|
464
438
|
|
|
465
439
|
// Update UI
|
|
466
|
-
this.ctx.chatContainer.clear();
|
|
467
440
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
468
441
|
await this.ctx.reloadTodos();
|
|
469
442
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
@@ -480,7 +453,6 @@ export class ExtensionUiController {
|
|
|
480
453
|
if (!result) {
|
|
481
454
|
return { cancelled: true };
|
|
482
455
|
}
|
|
483
|
-
this.ctx.chatContainer.clear();
|
|
484
456
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
485
457
|
await this.ctx.reloadTodos();
|
|
486
458
|
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
|
}
|
|
@@ -138,7 +154,6 @@ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
|
|
|
138
154
|
// deliberate human double-tap is always tens of milliseconds apart.
|
|
139
155
|
const LEFT_DOUBLE_TAP_MIN_GAP_MS = 40;
|
|
140
156
|
const LEFT_DOUBLE_TAP_MAX_GAP_MS = 500;
|
|
141
|
-
const STREAMING_ESCAPE_CANCEL_WINDOW_MS = 2_000;
|
|
142
157
|
|
|
143
158
|
export class InputController {
|
|
144
159
|
constructor(
|
|
@@ -163,16 +178,6 @@ export class InputController {
|
|
|
163
178
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
164
179
|
// #detectLeftDoubleTap.
|
|
165
180
|
#leftTapCount = 0;
|
|
166
|
-
// Streaming turns use a two-step Esc: first press arms this token, second press
|
|
167
|
-
// within the window aborts the same live assistant turn. The token is a per-turn
|
|
168
|
-
// sentinel minted lazily on demand and reset on every `agent_start`/`agent_end`
|
|
169
|
-
// (see setupKeyHandlers), so it survives `message_start`/`message_update`
|
|
170
|
-
// transitions inside a single turn but cannot leak across turn boundaries.
|
|
171
|
-
#streamingEscapeTurnSentinel: object | undefined;
|
|
172
|
-
#streamingEscapeArmedToken: object | undefined;
|
|
173
|
-
#streamingEscapeArmedUntil = 0;
|
|
174
|
-
#streamingEscapeTimer: NodeJS.Timeout | undefined;
|
|
175
|
-
#streamingEscapeSessionSubscribed = false;
|
|
176
181
|
// Sequential index for `local://attachment-N` references created by large-paste and
|
|
177
182
|
// pasted-file attachments. Seeded from 0 and bumped past existing attachment files.
|
|
178
183
|
#attachmentCounter = 0;
|
|
@@ -222,50 +227,12 @@ export class InputController {
|
|
|
222
227
|
const unsubscribe = tinyTitleClient.onProgress(update);
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
#
|
|
226
|
-
this
|
|
227
|
-
this.#streamingEscapeArmedUntil = 0;
|
|
228
|
-
if (this.#streamingEscapeTimer) {
|
|
229
|
-
clearTimeout(this.#streamingEscapeTimer);
|
|
230
|
-
this.#streamingEscapeTimer = undefined;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
#handleStreamingEscape(): void {
|
|
235
|
-
if (!this.#streamingEscapeTurnSentinel) {
|
|
236
|
-
this.#streamingEscapeTurnSentinel = {};
|
|
237
|
-
}
|
|
238
|
-
const token = this.#streamingEscapeTurnSentinel;
|
|
239
|
-
const now = Date.now();
|
|
240
|
-
if (this.#streamingEscapeArmedToken === token && now <= this.#streamingEscapeArmedUntil) {
|
|
241
|
-
this.#clearStreamingEscapeArm();
|
|
242
|
-
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this.#clearStreamingEscapeArm();
|
|
247
|
-
this.#streamingEscapeArmedToken = token;
|
|
248
|
-
this.#streamingEscapeArmedUntil = now + STREAMING_ESCAPE_CANCEL_WINDOW_MS;
|
|
249
|
-
this.#streamingEscapeTimer = setTimeout(() => {
|
|
250
|
-
if (this.#streamingEscapeArmedToken === token && Date.now() >= this.#streamingEscapeArmedUntil) {
|
|
251
|
-
this.#clearStreamingEscapeArm();
|
|
252
|
-
}
|
|
253
|
-
}, STREAMING_ESCAPE_CANCEL_WINDOW_MS);
|
|
254
|
-
this.#streamingEscapeTimer.unref?.();
|
|
255
|
-
this.ctx.showStatus("Press Esc again within 2s to cancel streaming.");
|
|
230
|
+
#abortStreamingTurn(): void {
|
|
231
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
256
232
|
}
|
|
257
233
|
|
|
258
234
|
setupKeyHandlers(): void {
|
|
259
235
|
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
260
|
-
if (!this.#streamingEscapeSessionSubscribed && typeof this.ctx.session.subscribe === "function") {
|
|
261
|
-
this.#streamingEscapeSessionSubscribed = true;
|
|
262
|
-
this.ctx.session.subscribe(event => {
|
|
263
|
-
if (event.type === "agent_start" || event.type === "agent_end") {
|
|
264
|
-
this.#streamingEscapeTurnSentinel = undefined;
|
|
265
|
-
this.#clearStreamingEscapeArm();
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
236
|
if (!this.#focusedLeftTapListenerInstalled) {
|
|
270
237
|
this.#focusedLeftTapListenerInstalled = true;
|
|
271
238
|
this.ctx.ui.addInputListener(data => {
|
|
@@ -335,7 +302,7 @@ export class InputController {
|
|
|
335
302
|
if (this.ctx.loopModeEnabled) {
|
|
336
303
|
this.ctx.pauseLoop();
|
|
337
304
|
if (this.ctx.session.isStreaming) {
|
|
338
|
-
this.#
|
|
305
|
+
this.#abortStreamingTurn();
|
|
339
306
|
} else {
|
|
340
307
|
this.ctx.cancelPendingSubmission();
|
|
341
308
|
}
|
|
@@ -386,11 +353,10 @@ export class InputController {
|
|
|
386
353
|
this.ctx.isPythonMode = false;
|
|
387
354
|
this.ctx.updateEditorBorderColor();
|
|
388
355
|
} else if (this.ctx.session.isStreaming) {
|
|
389
|
-
this.#
|
|
356
|
+
this.#abortStreamingTurn();
|
|
390
357
|
} else if (this.ctx.editor.getText().trim()) {
|
|
391
|
-
// Esc must not destroy an in-progress draft
|
|
358
|
+
// Esc must not destroy an in-progress draft.
|
|
392
359
|
this.ctx.lastEscapeTime = 0;
|
|
393
|
-
this.#clearStreamingEscapeArm();
|
|
394
360
|
} else if (vocalizer.isSpeaking()) {
|
|
395
361
|
// TTS buffers seconds of PCM past the streaming abort, so an Esc
|
|
396
362
|
// arriving after the model stopped would otherwise fall through to
|
|
@@ -536,7 +502,7 @@ export class InputController {
|
|
|
536
502
|
const wasPythonMode = this.ctx.isPythonMode;
|
|
537
503
|
const trimmed = text.trimStart();
|
|
538
504
|
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
539
|
-
this.ctx.isPythonMode =
|
|
505
|
+
this.ctx.isPythonMode = parsePythonCommandInput(trimmed) !== undefined;
|
|
540
506
|
if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
|
|
541
507
|
this.ctx.updateEditorBorderColor();
|
|
542
508
|
}
|
|
@@ -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");
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocomplete for GitHub issue/PR references typed as `#<number>` (e.g. `#3164`).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the `@` file-reference and `scheme://` internal-url conventions: the
|
|
5
|
+
* token is rewritten to an internal URL (`pr://3164` or `issue://3164`) plus a
|
|
6
|
+
* trailing space, and the existing tool-mediated pipeline (the `read` tool →
|
|
7
|
+
* InternalUrlRouter → `gh`) resolves it from the session cwd's git remote.
|
|
8
|
+
*
|
|
9
|
+
* No network at suggestion time — candidates are generated locally. GitHub
|
|
10
|
+
* shares the issue/PR number space and there is no cheap way to tell which a
|
|
11
|
+
* given number is while typing, so both a PR and an Issue candidate are offered
|
|
12
|
+
* by default. Naming the type first (`pr #3164` / `issue #3164`) constrains the
|
|
13
|
+
* candidates to that kind. Anything that is not a standalone `#<number>` token
|
|
14
|
+
* keeps falling through to the existing prompt-action menu.
|
|
15
|
+
*/
|
|
16
|
+
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
17
|
+
|
|
18
|
+
/** Candidate kinds, in default display order. */
|
|
19
|
+
const GITHUB_REF_KINDS = [
|
|
20
|
+
{ qualifier: "pr", scheme: "pr", label: "PR", description: "GitHub pull request" },
|
|
21
|
+
{ qualifier: "issue", scheme: "issue", label: "Issue", description: "GitHub issue" },
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export interface GithubRefContext {
|
|
25
|
+
/** Text to replace on accept: `#3164`, or `pr #3164` when a qualifier precedes it. */
|
|
26
|
+
prefix: string;
|
|
27
|
+
/** Type the user named (`pr`/`pull` → `pr`, `issue` → `issue`), or null to offer both. */
|
|
28
|
+
qualifier: "pr" | "issue" | null;
|
|
29
|
+
/** The numeric reference, e.g. `3164`. */
|
|
30
|
+
number: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A standalone `#<positive-number>` token ending at the cursor. The `#` must be
|
|
35
|
+
* preceded by a token boundary (start, whitespace, or an opening quote/paren/`<`/`=`,
|
|
36
|
+
* matching the internal-URL boundary set) so embedded hashes like `owner/repo#N`,
|
|
37
|
+
* `foo#N`, `C#12`, or a URL fragment do not match. An optional `pr`/`pull`/`issue`
|
|
38
|
+
* qualifier word (case-insensitive) immediately before the `#` constrains the kind.
|
|
39
|
+
*/
|
|
40
|
+
const GITHUB_REF_TOKEN_RE = /(?:^|[\s"'`(<=])(?:(pr|pull|issue)(\s+))?#([1-9]\d*)$/i;
|
|
41
|
+
|
|
42
|
+
export function getGithubRefContext(textBeforeCursor: string): GithubRefContext | null {
|
|
43
|
+
const match = textBeforeCursor.match(GITHUB_REF_TOKEN_RE);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
const qualifierWord = match[1];
|
|
46
|
+
const whitespace = match[2] ?? "";
|
|
47
|
+
const number = match[3] ?? "";
|
|
48
|
+
return {
|
|
49
|
+
prefix: qualifierWord ? `${qualifierWord}${whitespace}#${number}` : `#${number}`,
|
|
50
|
+
qualifier: !qualifierWord ? null : qualifierWord.toLowerCase() === "issue" ? "issue" : "pr",
|
|
51
|
+
number,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Suggestions for a `#<number>` token. Both kinds are offered unless the user
|
|
57
|
+
* named a type (`pr #3164` / `issue #3164`), in which case only that kind is
|
|
58
|
+
* offered. Returns `null` when the text before the cursor is not a standalone
|
|
59
|
+
* `#<number>` token.
|
|
60
|
+
*/
|
|
61
|
+
export function getGithubRefSuggestions(
|
|
62
|
+
textBeforeCursor: string,
|
|
63
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
64
|
+
const context = getGithubRefContext(textBeforeCursor);
|
|
65
|
+
if (!context) return null;
|
|
66
|
+
const kinds = context.qualifier
|
|
67
|
+
? GITHUB_REF_KINDS.filter(kind => kind.qualifier === context.qualifier)
|
|
68
|
+
: GITHUB_REF_KINDS;
|
|
69
|
+
const items: AutocompleteItem[] = kinds.map(kind => ({
|
|
70
|
+
value: `${kind.scheme}://${context.number}`,
|
|
71
|
+
label: `${kind.label} #${context.number}`,
|
|
72
|
+
description: kind.description,
|
|
73
|
+
}));
|
|
74
|
+
return { items, prefix: context.prefix };
|
|
75
|
+
}
|
|
@@ -16,6 +16,7 @@ import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
|
16
16
|
import type { AssistantMessage, ImageContent, Message, Model, Usage, UsageReport } from "@oh-my-pi/pi-ai";
|
|
17
17
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
18
18
|
import type {
|
|
19
|
+
AutocompleteProvider,
|
|
19
20
|
Component,
|
|
20
21
|
EditorTheme,
|
|
21
22
|
LoaderMessageColorFn,
|
|
@@ -59,6 +60,7 @@ import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
|
59
60
|
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
60
61
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
61
62
|
import type {
|
|
63
|
+
AutocompleteProviderFactory,
|
|
62
64
|
ContextUsage,
|
|
63
65
|
ExtensionUIContext,
|
|
64
66
|
ExtensionUIDialogOptions,
|
|
@@ -516,6 +518,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
516
518
|
collabGuest?: CollabGuestLink;
|
|
517
519
|
|
|
518
520
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
521
|
+
/** Built-in editor autocomplete provider, before extension wrapping. */
|
|
522
|
+
#baseAutocompleteProvider: AutocompleteProvider | undefined;
|
|
523
|
+
/** Extension-registered provider factories, applied in registration order (#4919). */
|
|
524
|
+
#autocompleteProviderFactories: AutocompleteProviderFactory[] = [];
|
|
519
525
|
#cleanupUnsubscribe?: () => void;
|
|
520
526
|
#signalTeardown?: SessionTeardown;
|
|
521
527
|
readonly #version: string;
|
|
@@ -579,10 +585,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
579
585
|
this.retryLoader.stop();
|
|
580
586
|
this.retryLoader = undefined;
|
|
581
587
|
}
|
|
582
|
-
this.statusContainer.
|
|
583
|
-
this.pendingMessagesContainer.
|
|
588
|
+
this.statusContainer.disposeChildren();
|
|
589
|
+
this.pendingMessagesContainer.disposeChildren();
|
|
584
590
|
this.#cancelModelCycleClearTimer();
|
|
585
|
-
this.modelCycleContainer.
|
|
591
|
+
this.modelCycleContainer.disposeChildren();
|
|
586
592
|
this.compactionQueuedMessages = [];
|
|
587
593
|
this.streamingComponent = undefined;
|
|
588
594
|
this.streamingMessage = undefined;
|
|
@@ -1094,14 +1100,49 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1094
1100
|
// source suffix (e.g. "Review code (project)"), so pass it through verbatim.
|
|
1095
1101
|
description: template.description,
|
|
1096
1102
|
}));
|
|
1097
|
-
|
|
1103
|
+
this.#baseAutocompleteProvider = this.#inputController.createAutocompleteProvider(
|
|
1098
1104
|
[...this.#pendingSlashCommands, ...fileSlashCommands, ...promptTemplateCommands],
|
|
1099
1105
|
basePath,
|
|
1100
1106
|
);
|
|
1101
|
-
this
|
|
1107
|
+
this.#applyAutocompleteProvider();
|
|
1102
1108
|
this.session.setSlashCommands(fileCommands);
|
|
1103
1109
|
}
|
|
1104
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* Rebuild the editor's autocomplete provider: the built-in provider wrapped
|
|
1113
|
+
* by every extension-registered factory, in registration order. A factory
|
|
1114
|
+
* that throws or returns a malformed provider is skipped so one broken
|
|
1115
|
+
* extension cannot take down core autocomplete.
|
|
1116
|
+
*/
|
|
1117
|
+
#applyAutocompleteProvider(): void {
|
|
1118
|
+
const base = this.#baseAutocompleteProvider;
|
|
1119
|
+
if (!base) return;
|
|
1120
|
+
let provider = base;
|
|
1121
|
+
for (const factory of this.#autocompleteProviderFactories) {
|
|
1122
|
+
try {
|
|
1123
|
+
const wrapped = factory(provider);
|
|
1124
|
+
if (
|
|
1125
|
+
wrapped &&
|
|
1126
|
+
typeof wrapped.getSuggestions === "function" &&
|
|
1127
|
+
typeof wrapped.applyCompletion === "function"
|
|
1128
|
+
) {
|
|
1129
|
+
provider = wrapped;
|
|
1130
|
+
} else {
|
|
1131
|
+
logger.warn("Extension autocomplete provider factory returned an invalid provider; skipping it");
|
|
1132
|
+
}
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
logger.warn("Extension autocomplete provider factory threw; skipping it", { error: String(error) });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
this.editor.setAutocompleteProvider(provider);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** Stack extension autocomplete behavior on top of the built-in editor provider (#4919). */
|
|
1141
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void {
|
|
1142
|
+
this.#autocompleteProviderFactories.push(factory);
|
|
1143
|
+
this.#applyAutocompleteProvider();
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1105
1146
|
/**
|
|
1106
1147
|
* Re-point the process and every cwd-derived cache at `newCwd` after the
|
|
1107
1148
|
* active session's working directory changed (`/move` relocation or resuming
|
|
@@ -2602,6 +2643,49 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2602
2643
|
}
|
|
2603
2644
|
}
|
|
2604
2645
|
|
|
2646
|
+
#resolveLocalRoot(): string {
|
|
2647
|
+
return resolveLocalUrlToPath("local://", {
|
|
2648
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2649
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
async #copyLocalArtifactsForFreshSession(sourceRoot: string, destinationRoot: string): Promise<void> {
|
|
2654
|
+
if (sourceRoot === destinationRoot) return;
|
|
2655
|
+
|
|
2656
|
+
let sourceRootStat: { isDirectory(): boolean };
|
|
2657
|
+
try {
|
|
2658
|
+
sourceRootStat = await fs.lstat(sourceRoot);
|
|
2659
|
+
} catch (error) {
|
|
2660
|
+
if (isEnoent(error)) return;
|
|
2661
|
+
throw error;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
if (!sourceRootStat.isDirectory()) return;
|
|
2665
|
+
|
|
2666
|
+
await fs.mkdir(destinationRoot, { recursive: true });
|
|
2667
|
+
await this.#copyLocalArtifactEntries(sourceRoot, destinationRoot);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
async #copyLocalArtifactEntries(sourceDir: string, destinationDir: string): Promise<void> {
|
|
2671
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
2672
|
+
for (const entry of entries) {
|
|
2673
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
2674
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
2675
|
+
|
|
2676
|
+
if (entry.isDirectory()) {
|
|
2677
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
2678
|
+
await this.#copyLocalArtifactEntries(sourcePath, destinationPath);
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
if (entry.isFile()) {
|
|
2683
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
2684
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2605
2689
|
async #approvePlan(
|
|
2606
2690
|
planContent: string,
|
|
2607
2691
|
options: {
|
|
@@ -2632,14 +2716,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2632
2716
|
});
|
|
2633
2717
|
|
|
2634
2718
|
if (!options.preserveContext) {
|
|
2719
|
+
const oldLocalRoot = this.#resolveLocalRoot();
|
|
2635
2720
|
await this.handleClearCommand();
|
|
2636
|
-
|
|
2637
|
-
|
|
2721
|
+
const newLocalRoot = this.#resolveLocalRoot();
|
|
2722
|
+
await this.#copyLocalArtifactsForFreshSession(oldLocalRoot, newLocalRoot);
|
|
2638
2723
|
const newLocalPath = resolveLocalUrlToPath(options.planFilePath, {
|
|
2639
2724
|
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
2640
2725
|
getSessionId: () => this.sessionManager.getSessionId(),
|
|
2641
2726
|
});
|
|
2642
|
-
await
|
|
2727
|
+
await fs.mkdir(path.dirname(newLocalPath), { recursive: true });
|
|
2728
|
+
await fs.writeFile(newLocalPath, planContent);
|
|
2643
2729
|
} else if (options.compactBeforeExecute) {
|
|
2644
2730
|
// Distill the plan-mode transcript before the execution turn is queued so
|
|
2645
2731
|
// the plan-approved synthetic prompt lands as a fresh cache anchor.
|
|
@@ -3626,7 +3712,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3626
3712
|
ensureLoadingAnimation(): void {
|
|
3627
3713
|
if (!this.loadingAnimation) {
|
|
3628
3714
|
this.#clearWorkingMessageAccentCache();
|
|
3629
|
-
this.statusContainer.
|
|
3715
|
+
this.statusContainer.disposeChildren();
|
|
3630
3716
|
const messageColorFn = ((message: string) =>
|
|
3631
3717
|
renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
|
|
3632
3718
|
animated?: true;
|
|
@@ -3647,7 +3733,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3647
3733
|
);
|
|
3648
3734
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
3649
3735
|
} else if (!this.statusContainer.children.includes(this.loadingAnimation)) {
|
|
3650
|
-
this.statusContainer.
|
|
3736
|
+
this.statusContainer.disposeChildren();
|
|
3651
3737
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
3652
3738
|
this.ui.requestRender();
|
|
3653
3739
|
}
|
|
@@ -3660,7 +3746,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3660
3746
|
this.loadingAnimation = undefined;
|
|
3661
3747
|
this.#clearWorkingMessageAccentCache();
|
|
3662
3748
|
if (clearStatusContainer) {
|
|
3663
|
-
this.statusContainer.
|
|
3749
|
+
this.statusContainer.disposeChildren();
|
|
3664
3750
|
}
|
|
3665
3751
|
}
|
|
3666
3752
|
|
|
@@ -4123,7 +4209,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4123
4209
|
}
|
|
4124
4210
|
this.#btwController.dispose();
|
|
4125
4211
|
this.#omfgController.dispose();
|
|
4126
|
-
this.chatContainer.clear();
|
|
4127
4212
|
this.renderInitialMessages({ clearTerminalHistory: true });
|
|
4128
4213
|
this.updateEditorBorderColor();
|
|
4129
4214
|
this.showStatus(
|