@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.6
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 +69 -1
- package/dist/types/config/settings-schema.d.ts +12 -22
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +10 -7
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +11 -10
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +13 -24
- package/src/config/settings.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/mcp/manager.ts +87 -4
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +38 -28
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +75 -103
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ask.ts +74 -32
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/index.ts +7 -37
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +12 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +42 -79
- package/src/utils/git.ts +9 -3
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
extractPrintableText,
|
|
8
8
|
fuzzyFilter,
|
|
9
9
|
Markdown,
|
|
10
|
+
type MarkdownTheme,
|
|
10
11
|
matchesKey,
|
|
11
12
|
padding,
|
|
12
13
|
renderInlineMarkdown,
|
|
@@ -14,8 +15,8 @@ import {
|
|
|
14
15
|
Spacer,
|
|
15
16
|
Text,
|
|
16
17
|
type TUI,
|
|
17
|
-
truncateToWidth,
|
|
18
18
|
visibleWidth,
|
|
19
|
+
wrapTextWithAnsi,
|
|
19
20
|
} from "@oh-my-pi/pi-tui";
|
|
20
21
|
import { getMarkdownTheme, type ThemeColor, theme } from "../../modes/theme/theme";
|
|
21
22
|
import {
|
|
@@ -69,6 +70,34 @@ export interface HookSelectorOptions {
|
|
|
69
70
|
slider?: HookSelectorSlider;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export interface HookSelectorOption {
|
|
74
|
+
label: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type HookSelectorOptionInput = string | HookSelectorOption;
|
|
79
|
+
|
|
80
|
+
function normalizeHookSelectorOption(option: HookSelectorOptionInput): HookSelectorOption {
|
|
81
|
+
if (typeof option === "string") return { label: option };
|
|
82
|
+
if (option.description?.trim()) {
|
|
83
|
+
return { label: option.label, description: option.description.trim() };
|
|
84
|
+
}
|
|
85
|
+
return { label: option.label };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function splitLeadingSpacesForWrap(line: string, width: number): { indent: string; body: string } {
|
|
89
|
+
let indentLength = 0;
|
|
90
|
+
while (indentLength < line.length && line.charCodeAt(indentLength) === 32) {
|
|
91
|
+
indentLength += 1;
|
|
92
|
+
}
|
|
93
|
+
const maxIndentLength = Math.max(0, width - 1);
|
|
94
|
+
const clampedIndentLength = Math.min(indentLength, maxIndentLength);
|
|
95
|
+
return {
|
|
96
|
+
indent: line.slice(0, clampedIndentLength),
|
|
97
|
+
body: line.slice(indentLength),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
class OutlinedList extends Container {
|
|
73
102
|
#lines: string[] = [];
|
|
74
103
|
|
|
@@ -81,19 +110,26 @@ class OutlinedList extends Container {
|
|
|
81
110
|
const borderColor = (text: string) => theme.fg("border", text);
|
|
82
111
|
const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
|
|
83
112
|
const innerWidth = Math.max(1, width - 2);
|
|
84
|
-
const content =
|
|
113
|
+
const content: string[] = [];
|
|
114
|
+
for (const line of this.#lines) {
|
|
85
115
|
const normalized = replaceTabs(line);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
const { indent, body } = splitLeadingSpacesForWrap(normalized, innerWidth);
|
|
117
|
+
const wrapped = wrapTextWithAnsi(body, Math.max(1, innerWidth - visibleWidth(indent)));
|
|
118
|
+
for (const wrappedBody of wrapped.length > 0 ? wrapped : [""]) {
|
|
119
|
+
const wrappedLine = `${indent}${wrappedBody}`;
|
|
120
|
+
const pad = Math.max(0, innerWidth - visibleWidth(wrappedLine));
|
|
121
|
+
content.push(
|
|
122
|
+
`${borderColor(theme.boxSharp.vertical)}${wrappedLine}${padding(pad)}${borderColor(theme.boxSharp.vertical)}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
90
126
|
return [horizontal, ...content, horizontal];
|
|
91
127
|
}
|
|
92
128
|
}
|
|
93
129
|
|
|
94
130
|
export class HookSelectorComponent extends Container {
|
|
95
|
-
#options:
|
|
96
|
-
#filteredOptions:
|
|
131
|
+
#options: HookSelectorOption[];
|
|
132
|
+
#filteredOptions: HookSelectorOption[];
|
|
97
133
|
#searchQuery = "";
|
|
98
134
|
#selectedIndex: number;
|
|
99
135
|
#maxVisible: number;
|
|
@@ -110,17 +146,18 @@ export class HookSelectorComponent extends Container {
|
|
|
110
146
|
#slider: HookSelectorSlider | undefined;
|
|
111
147
|
#sliderIndex: number = 0;
|
|
112
148
|
#sliderComponent: Text | undefined;
|
|
149
|
+
#lastRenderWidth: number | undefined;
|
|
113
150
|
constructor(
|
|
114
151
|
title: string,
|
|
115
|
-
options:
|
|
152
|
+
options: HookSelectorOptionInput[],
|
|
116
153
|
onSelect: (option: string) => void,
|
|
117
154
|
onCancel: () => void,
|
|
118
155
|
opts?: HookSelectorOptions,
|
|
119
156
|
) {
|
|
120
157
|
super();
|
|
121
158
|
|
|
122
|
-
this.#options = options;
|
|
123
|
-
this.#filteredOptions = options;
|
|
159
|
+
this.#options = options.map(normalizeHookSelectorOption);
|
|
160
|
+
this.#filteredOptions = this.#options;
|
|
124
161
|
this.#selectedIndex = Math.min(opts?.initialIndex ?? 0, this.#filteredOptions.length - 1);
|
|
125
162
|
this.#maxVisible = Math.max(3, opts?.maxVisible ?? 12);
|
|
126
163
|
this.#onSelectCallback = onSelect;
|
|
@@ -156,7 +193,7 @@ export class HookSelectorComponent extends Container {
|
|
|
156
193
|
opts?.onTimeout?.();
|
|
157
194
|
const selected = this.#filteredOptions[this.#selectedIndex];
|
|
158
195
|
if (selected) {
|
|
159
|
-
this.#onSelectCallback(selected);
|
|
196
|
+
this.#onSelectCallback(selected.label);
|
|
160
197
|
} else {
|
|
161
198
|
this.#onCancelCallback();
|
|
162
199
|
}
|
|
@@ -180,32 +217,111 @@ export class HookSelectorComponent extends Container {
|
|
|
180
217
|
this.#updateList();
|
|
181
218
|
}
|
|
182
219
|
|
|
183
|
-
#
|
|
220
|
+
#renderOptionLines(option: HookSelectorOption, isSelected: boolean, mdTheme: MarkdownTheme): string[] {
|
|
221
|
+
const label = isSelected
|
|
222
|
+
? renderInlineMarkdown(option.label, mdTheme, t => theme.fg("accent", t))
|
|
223
|
+
: renderInlineMarkdown(option.label, mdTheme, t => theme.fg("text", t));
|
|
224
|
+
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
225
|
+
const lines = [prefix + label];
|
|
226
|
+
if (option.description) {
|
|
227
|
+
const description = renderInlineMarkdown(option.description, mdTheme, t => theme.fg("muted", t));
|
|
228
|
+
lines.push(` ${description}`);
|
|
229
|
+
}
|
|
230
|
+
return lines;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#renderedLineRowCount(line: string, renderWidth: number): number {
|
|
234
|
+
const normalized = replaceTabs(line);
|
|
235
|
+
if (this.#outlinedList) {
|
|
236
|
+
const innerWidth = Math.max(1, renderWidth - 2);
|
|
237
|
+
const { indent, body } = splitLeadingSpacesForWrap(normalized, innerWidth);
|
|
238
|
+
const wrapped = wrapTextWithAnsi(body, Math.max(1, innerWidth - visibleWidth(indent)));
|
|
239
|
+
return Math.max(1, wrapped.length);
|
|
240
|
+
}
|
|
241
|
+
const wrapped = wrapTextWithAnsi(normalized, Math.max(1, renderWidth - 2));
|
|
242
|
+
return Math.max(1, wrapped.length);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#optionRowCount(
|
|
246
|
+
option: HookSelectorOption,
|
|
247
|
+
renderWidth: number | undefined,
|
|
248
|
+
isSelected: boolean,
|
|
249
|
+
mdTheme: MarkdownTheme,
|
|
250
|
+
): number {
|
|
251
|
+
if (renderWidth === undefined) return option.description ? 2 : 1;
|
|
252
|
+
let rows = 0;
|
|
253
|
+
for (const line of this.#renderOptionLines(option, isSelected, mdTheme)) {
|
|
254
|
+
rows += this.#renderedLineRowCount(line, renderWidth);
|
|
255
|
+
}
|
|
256
|
+
return rows;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#totalOptionRows(options: HookSelectorOption[], renderWidth?: number, mdTheme?: MarkdownTheme): number {
|
|
260
|
+
const themeForRows = mdTheme ?? getMarkdownTheme();
|
|
261
|
+
let rows = 0;
|
|
262
|
+
for (const option of options) {
|
|
263
|
+
rows += this.#optionRowCount(option, renderWidth, false, themeForRows);
|
|
264
|
+
}
|
|
265
|
+
return rows;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#getVisibleOptionRange(
|
|
269
|
+
total: number,
|
|
270
|
+
renderWidth?: number,
|
|
271
|
+
mdTheme: MarkdownTheme = getMarkdownTheme(),
|
|
272
|
+
): { startIndex: number; endIndex: number } {
|
|
273
|
+
if (total === 0) return { startIndex: 0, endIndex: 0 };
|
|
274
|
+
|
|
275
|
+
const rowBudget = Math.max(1, this.#maxVisible);
|
|
276
|
+
const selectedIndex = Math.max(0, Math.min(this.#selectedIndex, total - 1));
|
|
277
|
+
let startIndex = selectedIndex;
|
|
278
|
+
let endIndex = selectedIndex + 1;
|
|
279
|
+
let rows = this.#optionRowCount(this.#filteredOptions[selectedIndex]!, renderWidth, true, mdTheme);
|
|
280
|
+
let beforeRows = 0;
|
|
281
|
+
const targetBeforeRows = Math.max(0, Math.floor((rowBudget - rows) / 2));
|
|
282
|
+
|
|
283
|
+
while (startIndex > 0) {
|
|
284
|
+
const cost = this.#optionRowCount(this.#filteredOptions[startIndex - 1]!, renderWidth, false, mdTheme);
|
|
285
|
+
if (beforeRows + cost > targetBeforeRows || rows + cost > rowBudget) break;
|
|
286
|
+
startIndex--;
|
|
287
|
+
beforeRows += cost;
|
|
288
|
+
rows += cost;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
while (endIndex < total) {
|
|
292
|
+
const cost = this.#optionRowCount(this.#filteredOptions[endIndex]!, renderWidth, false, mdTheme);
|
|
293
|
+
if (rows + cost > rowBudget) break;
|
|
294
|
+
endIndex++;
|
|
295
|
+
rows += cost;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
while (startIndex > 0) {
|
|
299
|
+
const cost = this.#optionRowCount(this.#filteredOptions[startIndex - 1]!, renderWidth, false, mdTheme);
|
|
300
|
+
if (rows + cost > rowBudget) break;
|
|
301
|
+
startIndex--;
|
|
302
|
+
rows += cost;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { startIndex, endIndex };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#updateList(renderWidth = this.#lastRenderWidth): void {
|
|
184
309
|
const lines: string[] = [];
|
|
185
310
|
const total = this.#filteredOptions.length;
|
|
186
|
-
const startIndex = Math.max(
|
|
187
|
-
0,
|
|
188
|
-
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), total - this.#maxVisible),
|
|
189
|
-
);
|
|
190
|
-
const endIndex = Math.min(startIndex + this.#maxVisible, total);
|
|
191
|
-
|
|
192
311
|
const mdTheme = getMarkdownTheme();
|
|
312
|
+
const { startIndex, endIndex } = this.#getVisibleOptionRange(total, renderWidth, mdTheme);
|
|
313
|
+
|
|
193
314
|
for (let i = startIndex; i < endIndex; i++) {
|
|
194
315
|
const option = this.#filteredOptions[i];
|
|
195
316
|
if (option === undefined) continue;
|
|
196
|
-
|
|
197
|
-
const label = isSelected
|
|
198
|
-
? renderInlineMarkdown(option, mdTheme, t => theme.fg("accent", t))
|
|
199
|
-
: renderInlineMarkdown(option, mdTheme, t => theme.fg("text", t));
|
|
200
|
-
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
201
|
-
lines.push(prefix + label);
|
|
317
|
+
lines.push(...this.#renderOptionLines(option, i === this.#selectedIndex, mdTheme));
|
|
202
318
|
}
|
|
203
319
|
|
|
204
320
|
if (total === 0) {
|
|
205
321
|
lines.push(theme.fg("dim", " No matching options"));
|
|
206
322
|
}
|
|
207
323
|
|
|
208
|
-
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
|
|
324
|
+
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus(renderWidth, mdTheme)) {
|
|
209
325
|
lines.push(this.#renderStatusLine(total));
|
|
210
326
|
}
|
|
211
327
|
if (this.#outlinedList) {
|
|
@@ -253,12 +369,12 @@ export class HookSelectorComponent extends Container {
|
|
|
253
369
|
slider.onChange?.(next);
|
|
254
370
|
}
|
|
255
371
|
|
|
256
|
-
#isSearchEnabled(): boolean {
|
|
257
|
-
return this.#options
|
|
372
|
+
#isSearchEnabled(renderWidth = this.#lastRenderWidth, mdTheme?: MarkdownTheme): boolean {
|
|
373
|
+
return this.#totalOptionRows(this.#options, renderWidth, mdTheme) > this.#maxVisible;
|
|
258
374
|
}
|
|
259
375
|
|
|
260
|
-
#shouldRenderSearchStatus(): boolean {
|
|
261
|
-
return this.#isSearchEnabled() || this.#searchQuery.length > 0;
|
|
376
|
+
#shouldRenderSearchStatus(renderWidth = this.#lastRenderWidth, mdTheme?: MarkdownTheme): boolean {
|
|
377
|
+
return this.#isSearchEnabled(renderWidth, mdTheme) || this.#searchQuery.length > 0;
|
|
262
378
|
}
|
|
263
379
|
|
|
264
380
|
#renderStatusLine(total: number): string {
|
|
@@ -273,7 +389,9 @@ export class HookSelectorComponent extends Container {
|
|
|
273
389
|
|
|
274
390
|
#setSearchQuery(query: string): void {
|
|
275
391
|
this.#searchQuery = query;
|
|
276
|
-
this.#filteredOptions = query.trim()
|
|
392
|
+
this.#filteredOptions = query.trim()
|
|
393
|
+
? fuzzyFilter(this.#options, query, option => `${option.label} ${option.description ?? ""}`)
|
|
394
|
+
: this.#options;
|
|
277
395
|
this.#selectedIndex = 0;
|
|
278
396
|
this.#updateList();
|
|
279
397
|
}
|
|
@@ -322,7 +440,7 @@ export class HookSelectorComponent extends Container {
|
|
|
322
440
|
}
|
|
323
441
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
324
442
|
const selected = this.#filteredOptions[this.#selectedIndex];
|
|
325
|
-
if (selected) this.#onSelectCallback(selected);
|
|
443
|
+
if (selected) this.#onSelectCallback(selected.label);
|
|
326
444
|
} else if (matchesKey(keyData, "left") || (this.#slider && !this.#isSearchEnabled() && keyData === "h")) {
|
|
327
445
|
if (this.#slider) this.#moveSlider(-1);
|
|
328
446
|
else this.#onLeftCallback?.();
|
|
@@ -334,6 +452,15 @@ export class HookSelectorComponent extends Container {
|
|
|
334
452
|
}
|
|
335
453
|
}
|
|
336
454
|
|
|
455
|
+
override render(width: number): string[] {
|
|
456
|
+
const renderWidth = Math.max(1, width);
|
|
457
|
+
if (this.#lastRenderWidth !== renderWidth) {
|
|
458
|
+
this.#lastRenderWidth = renderWidth;
|
|
459
|
+
this.#updateList(renderWidth);
|
|
460
|
+
}
|
|
461
|
+
return super.render(renderWidth);
|
|
462
|
+
}
|
|
463
|
+
|
|
337
464
|
dispose(): void {
|
|
338
465
|
this.#countdown?.dispose();
|
|
339
466
|
}
|
|
@@ -141,6 +141,14 @@ export interface ToolExecutionHandle {
|
|
|
141
141
|
setExpanded(expanded: boolean): void;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** Drive pending-tool redraws at ~60fps so the animated border sweep is smooth.
|
|
145
|
+
* The TUI already throttles at its 16ms `MIN_RENDER_INTERVAL_MS`, so this is the
|
|
146
|
+
* natural upper bound and static frames diff to a no-op redraw at ~zero cost. */
|
|
147
|
+
const SPINNER_RENDER_INTERVAL_MS = 16;
|
|
148
|
+
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
|
149
|
+
* 60fps render cadence (mirrors `Loader`). */
|
|
150
|
+
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
151
|
+
|
|
144
152
|
/**
|
|
145
153
|
* Component that renders a tool call with its result (updateable)
|
|
146
154
|
*/
|
|
@@ -177,6 +185,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
177
185
|
// Spinner animation for partial task results
|
|
178
186
|
#spinnerFrame?: number;
|
|
179
187
|
#spinnerInterval?: NodeJS.Timeout;
|
|
188
|
+
#lastSpinnerAdvanceAt = 0;
|
|
180
189
|
// Todo write completion strikethrough reveal animation
|
|
181
190
|
#todoStrikeInterval?: NodeJS.Timeout;
|
|
182
191
|
// Track if args are still being streamed (for edit/write spinner)
|
|
@@ -404,13 +413,20 @@ export class ToolExecutionComponent extends Container {
|
|
|
404
413
|
this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
|
|
405
414
|
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
406
415
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
416
|
+
this.#lastSpinnerAdvanceAt = performance.now();
|
|
407
417
|
this.#spinnerInterval = setInterval(() => {
|
|
418
|
+
const now = performance.now();
|
|
408
419
|
const frameCount = theme.spinnerFrames.length;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
420
|
+
// Redraw at ~60fps for a smooth border sweep, but only step the spinner
|
|
421
|
+
// glyph at its classic ~12.5fps cadence. The TUI throttles renders at
|
|
422
|
+
// 16ms and the differ drops no-op redraws, so the extra ticks are free.
|
|
423
|
+
if (frameCount > 0 && now - this.#lastSpinnerAdvanceAt >= SPINNER_GLYPH_ADVANCE_MS) {
|
|
424
|
+
this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
|
|
425
|
+
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
426
|
+
this.#lastSpinnerAdvanceAt = now;
|
|
427
|
+
}
|
|
412
428
|
this.#ui.requestRender();
|
|
413
|
-
},
|
|
429
|
+
}, SPINNER_RENDER_INTERVAL_MS);
|
|
414
430
|
} else if (!needsSpinner && this.#spinnerInterval) {
|
|
415
431
|
clearInterval(this.#spinnerInterval);
|
|
416
432
|
this.#spinnerInterval = undefined;
|
|
@@ -1124,48 +1124,16 @@ export class CommandController {
|
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
1126
|
/**
|
|
1127
|
-
* TUI handler for `/shake`. `elide
|
|
1128
|
-
* `
|
|
1129
|
-
* (Esc aborts via `abortCompaction`). Rebuilds the chat and reports counts.
|
|
1127
|
+
* TUI handler for `/shake`. `elide` drops heavy structural content and
|
|
1128
|
+
* `images` strips image blocks. Rebuilds the chat and reports counts.
|
|
1130
1129
|
*/
|
|
1131
1130
|
async handleShakeCommand(mode: ShakeMode): Promise<void> {
|
|
1132
1131
|
let result: ShakeResult;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
this.ctx.statusContainer.clear();
|
|
1139
|
-
const originalOnEscape = this.ctx.editor.onEscape;
|
|
1140
|
-
this.ctx.editor.onEscape = () => {
|
|
1141
|
-
this.ctx.session.abortCompaction();
|
|
1142
|
-
};
|
|
1143
|
-
const loader = new Loader(
|
|
1144
|
-
this.ctx.ui,
|
|
1145
|
-
spinner => theme.fg("accent", spinner),
|
|
1146
|
-
text => theme.fg("muted", text),
|
|
1147
|
-
"Shaking context (summary)… (esc to cancel)",
|
|
1148
|
-
getSymbolTheme().spinnerFrames,
|
|
1149
|
-
);
|
|
1150
|
-
this.ctx.statusContainer.addChild(loader);
|
|
1151
|
-
this.ctx.ui.requestRender();
|
|
1152
|
-
try {
|
|
1153
|
-
result = await this.ctx.session.shake("summary");
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1156
|
-
return;
|
|
1157
|
-
} finally {
|
|
1158
|
-
loader.stop();
|
|
1159
|
-
this.ctx.statusContainer.clear();
|
|
1160
|
-
this.ctx.editor.onEscape = originalOnEscape;
|
|
1161
|
-
}
|
|
1162
|
-
} else {
|
|
1163
|
-
try {
|
|
1164
|
-
result = await this.ctx.session.shake(mode);
|
|
1165
|
-
} catch (error) {
|
|
1166
|
-
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1132
|
+
try {
|
|
1133
|
+
result = await this.ctx.session.shake(mode);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1136
|
+
return;
|
|
1169
1137
|
}
|
|
1170
1138
|
|
|
1171
1139
|
const dropped = result.toolResultsDropped + result.blocksDropped + (result.imagesDropped ?? 0);
|
|
@@ -25,12 +25,14 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
25
25
|
|
|
26
26
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
27
27
|
|
|
28
|
-
// Events that change
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const TOOL_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
28
|
+
// Events that change foreground streaming state, or that reset a turn. The TUI
|
|
29
|
+
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
|
30
|
+
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
|
31
|
+
const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
33
32
|
agent_start: true,
|
|
33
|
+
agent_end: true,
|
|
34
|
+
message_start: true,
|
|
35
|
+
message_end: true,
|
|
34
36
|
tool_execution_start: true,
|
|
35
37
|
tool_execution_update: true,
|
|
36
38
|
tool_execution_end: true,
|
|
@@ -46,6 +48,7 @@ export class EventController {
|
|
|
46
48
|
#renderedCustomMessages = new Set<string>();
|
|
47
49
|
#lastIntent: string | undefined = undefined;
|
|
48
50
|
#backgroundToolCallIds = new Set<string>();
|
|
51
|
+
#assistantMessageStreaming = false;
|
|
49
52
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
50
53
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
51
54
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
@@ -169,24 +172,27 @@ export class EventController {
|
|
|
169
172
|
|
|
170
173
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
171
174
|
await run(event);
|
|
172
|
-
// While a foreground tool is
|
|
173
|
-
// re-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
175
|
+
// While assistant text or a foreground tool is streaming, rows above the
|
|
176
|
+
// viewport can re-layout after they have already entered native scrollback
|
|
177
|
+
// (Markdown fences, wrapping, previews). Let the TUI rebuild history on
|
|
178
|
+
// those offscreen edits instead of deferring, which otherwise leaves stale
|
|
179
|
+
// tail rows duplicated above the live viewport.
|
|
180
|
+
// Background-running tools are excluded so late async updates outside the
|
|
181
|
+
// active foreground stream keep the no-yank deferral; agent_start resets
|
|
182
|
+
// the mode at every turn boundary.
|
|
183
|
+
if (STREAM_RENDER_MODE_EVENTS[event.type]) {
|
|
180
184
|
this.#refreshToolRenderMode();
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
#refreshToolRenderMode(): void {
|
|
185
|
-
let foregroundToolActive =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
let foregroundToolActive = this.#assistantMessageStreaming;
|
|
190
|
+
if (!foregroundToolActive) {
|
|
191
|
+
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
|
192
|
+
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
|
193
|
+
foregroundToolActive = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
|
@@ -196,6 +202,7 @@ export class EventController {
|
|
|
196
202
|
this.#lastIntent = undefined;
|
|
197
203
|
this.#readToolCallArgs.clear();
|
|
198
204
|
this.#readToolCallAssistantComponents.clear();
|
|
205
|
+
this.#assistantMessageStreaming = false;
|
|
199
206
|
this.#lastAssistantComponent = undefined;
|
|
200
207
|
if (this.ctx.retryEscapeHandler) {
|
|
201
208
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
@@ -268,9 +275,13 @@ export class EventController {
|
|
|
268
275
|
this.ctx.ui.requestRender();
|
|
269
276
|
} else if (event.message.role === "assistant") {
|
|
270
277
|
this.#lastThinkingCount = 0;
|
|
278
|
+
this.#assistantMessageStreaming = true;
|
|
271
279
|
this.#resetReadGroup();
|
|
272
|
-
this.ctx.streamingComponent = new AssistantMessageComponent(
|
|
273
|
-
|
|
280
|
+
this.ctx.streamingComponent = new AssistantMessageComponent(
|
|
281
|
+
undefined,
|
|
282
|
+
this.ctx.hideThinkingBlock,
|
|
283
|
+
() => this.ctx.ui.requestRender(),
|
|
284
|
+
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
274
285
|
);
|
|
275
286
|
this.ctx.streamingMessage = event.message;
|
|
276
287
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -414,6 +425,9 @@ export class EventController {
|
|
|
414
425
|
|
|
415
426
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
416
427
|
if (event.message.role === "user") return;
|
|
428
|
+
if (event.message.role === "assistant") {
|
|
429
|
+
this.#assistantMessageStreaming = false;
|
|
430
|
+
}
|
|
417
431
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
418
432
|
this.ctx.streamingMessage = event.message;
|
|
419
433
|
let errorMessage: string | undefined;
|
|
@@ -596,8 +610,8 @@ export class EventController {
|
|
|
596
610
|
}
|
|
597
611
|
}
|
|
598
612
|
}
|
|
599
|
-
|
|
600
613
|
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
614
|
+
this.#assistantMessageStreaming = false;
|
|
601
615
|
if (this.ctx.loadingAnimation) {
|
|
602
616
|
this.ctx.loadingAnimation.stop();
|
|
603
617
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -647,9 +661,7 @@ export class EventController {
|
|
|
647
661
|
? "Auto-handoff"
|
|
648
662
|
: event.action === "shake"
|
|
649
663
|
? "Auto-shake"
|
|
650
|
-
:
|
|
651
|
-
? "Auto-shake (summary)"
|
|
652
|
-
: "Auto context-full maintenance";
|
|
664
|
+
: "Auto context-full maintenance";
|
|
653
665
|
this.ctx.autoCompactionLoader = new Loader(
|
|
654
666
|
this.ctx.ui,
|
|
655
667
|
spinner => theme.fg("accent", spinner),
|
|
@@ -673,7 +685,7 @@ export class EventController {
|
|
|
673
685
|
this.ctx.statusContainer.clear();
|
|
674
686
|
}
|
|
675
687
|
const isHandoffAction = event.action === "handoff";
|
|
676
|
-
const isShakeAction = event.action === "shake"
|
|
688
|
+
const isShakeAction = event.action === "shake";
|
|
677
689
|
if (event.aborted) {
|
|
678
690
|
this.ctx.showStatus(
|
|
679
691
|
isHandoffAction
|
|
@@ -690,9 +702,7 @@ export class EventController {
|
|
|
690
702
|
this.ctx.rebuildChatFromMessages();
|
|
691
703
|
this.ctx.statusLine.invalidate();
|
|
692
704
|
this.ctx.updateEditorTopBorder();
|
|
693
|
-
this.ctx.showStatus(
|
|
694
|
-
event.action === "shake-summary" ? "Auto-shake (summary) completed" : "Auto-shake completed",
|
|
695
|
-
);
|
|
705
|
+
this.ctx.showStatus("Auto-shake completed");
|
|
696
706
|
}
|
|
697
707
|
} else if (event.result) {
|
|
698
708
|
this.ctx.rebuildChatFromMessages();
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ExtensionError,
|
|
11
11
|
ExtensionUIContext,
|
|
12
12
|
ExtensionUIDialogOptions,
|
|
13
|
+
ExtensionUISelectItem,
|
|
13
14
|
ExtensionUiComponent,
|
|
14
15
|
ExtensionWidgetContent,
|
|
15
16
|
ExtensionWidgetOptions,
|
|
@@ -483,7 +484,7 @@ export class ExtensionUiController {
|
|
|
483
484
|
|
|
484
485
|
createBackgroundUiContext(): ExtensionUIContext {
|
|
485
486
|
return {
|
|
486
|
-
select: async (_title: string, _options:
|
|
487
|
+
select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined,
|
|
487
488
|
confirm: async (_title: string, _message: string, _dialogOptions) => false,
|
|
488
489
|
input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
|
|
489
490
|
notify: () => {},
|
|
@@ -581,7 +582,7 @@ export class ExtensionUiController {
|
|
|
581
582
|
*/
|
|
582
583
|
showHookSelector(
|
|
583
584
|
title: string,
|
|
584
|
-
options:
|
|
585
|
+
options: ExtensionUISelectItem[],
|
|
585
586
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
586
587
|
extra?: { slider?: HookSelectorSlider },
|
|
587
588
|
): Promise<string | undefined> {
|
|
@@ -86,21 +86,6 @@ export class InputController {
|
|
|
86
86
|
|
|
87
87
|
setupKeyHandlers(): void {
|
|
88
88
|
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
89
|
-
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
|
90
|
-
Boolean(
|
|
91
|
-
this.ctx.loadingAnimation ||
|
|
92
|
-
this.ctx.hasActiveBtw() ||
|
|
93
|
-
this.ctx.hasActiveOmfg() ||
|
|
94
|
-
this.ctx.session.isStreaming ||
|
|
95
|
-
this.ctx.session.isCompacting ||
|
|
96
|
-
this.ctx.session.isGeneratingHandoff ||
|
|
97
|
-
this.ctx.session.isBashRunning ||
|
|
98
|
-
this.ctx.session.isEvalRunning ||
|
|
99
|
-
this.ctx.autoCompactionLoader ||
|
|
100
|
-
this.ctx.retryLoader ||
|
|
101
|
-
this.ctx.autoCompactionEscapeHandler ||
|
|
102
|
-
this.ctx.retryEscapeHandler,
|
|
103
|
-
);
|
|
104
89
|
this.ctx.editor.onEscape = () => {
|
|
105
90
|
if (this.ctx.loopModeEnabled) {
|
|
106
91
|
this.ctx.pauseLoop();
|
|
@@ -1425,7 +1425,7 @@ export class MCPCommandController {
|
|
|
1425
1425
|
this.#showMessage(["", theme.fg("muted", `Reconnecting to "${name}"...`), ""].join("\n"));
|
|
1426
1426
|
|
|
1427
1427
|
try {
|
|
1428
|
-
const connection = await this.ctx.mcpManager.reconnectServer(name);
|
|
1428
|
+
const connection = await this.ctx.mcpManager.reconnectServer(name, { manual: true });
|
|
1429
1429
|
if (connection) {
|
|
1430
1430
|
// refreshMCPTools re-registers tools and preserves the user's prior
|
|
1431
1431
|
// MCP tool selection. No need to call activateDiscoveredMCPTools —
|
|
@@ -40,6 +40,7 @@ import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
|
40
40
|
import type {
|
|
41
41
|
ExtensionUIContext,
|
|
42
42
|
ExtensionUIDialogOptions,
|
|
43
|
+
ExtensionUISelectItem,
|
|
43
44
|
ExtensionWidgetContent,
|
|
44
45
|
ExtensionWidgetOptions,
|
|
45
46
|
} from "../extensibility/extensions";
|
|
@@ -2896,7 +2897,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2896
2897
|
|
|
2897
2898
|
showHookSelector(
|
|
2898
2899
|
title: string,
|
|
2899
|
-
options:
|
|
2900
|
+
options: ExtensionUISelectItem[],
|
|
2900
2901
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
2901
2902
|
extra?: { slider?: HookSelectorSlider },
|
|
2902
2903
|
): Promise<string | undefined> {
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
14
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import
|
|
16
|
-
ExtensionUIContext,
|
|
17
|
-
ExtensionUIDialogOptions,
|
|
18
|
-
|
|
15
|
+
import {
|
|
16
|
+
type ExtensionUIContext,
|
|
17
|
+
type ExtensionUIDialogOptions,
|
|
18
|
+
type ExtensionUISelectItem,
|
|
19
|
+
type ExtensionWidgetOptions,
|
|
20
|
+
getExtensionUISelectOptionLabel,
|
|
19
21
|
} from "../../extensibility/extensions";
|
|
20
22
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
21
23
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -256,11 +258,20 @@ export async function runRpcMode(
|
|
|
256
258
|
return promise;
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
select(
|
|
261
|
+
select(
|
|
262
|
+
title: string,
|
|
263
|
+
options: ExtensionUISelectItem[],
|
|
264
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
265
|
+
): Promise<string | undefined> {
|
|
260
266
|
return this.#createDialogPromise(
|
|
261
267
|
dialogOptions,
|
|
262
268
|
undefined,
|
|
263
|
-
{
|
|
269
|
+
{
|
|
270
|
+
method: "select",
|
|
271
|
+
title,
|
|
272
|
+
options: options.map(getExtensionUISelectOptionLabel),
|
|
273
|
+
timeout: dialogOptions?.timeout,
|
|
274
|
+
},
|
|
264
275
|
response => parseValueDialogResponse(response, dialogOptions),
|
|
265
276
|
);
|
|
266
277
|
}
|