@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.0
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 +159 -182
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- 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/plugins/legacy-pi-compat.d.ts +11 -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/main.d.ts +5 -0
- 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 +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- 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 +5 -3
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +12 -3
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- 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 +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- 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/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- 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/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- 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 +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +127 -57
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- 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 +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -15,9 +15,52 @@ import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
|
15
15
|
import { theme } from "../../modes/theme/theme";
|
|
16
16
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
17
17
|
import type { SessionInfo } from "../../session/session-manager";
|
|
18
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
18
19
|
import { DynamicBorder } from "./dynamic-border";
|
|
19
20
|
import { HookSelectorComponent } from "./hook-selector";
|
|
20
21
|
|
|
22
|
+
/** Returns the IDs of sessions whose recorded prompts match a query, best first. */
|
|
23
|
+
export type SessionHistoryMatcher = (query: string) => string[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Combine fuzzy session matches with prompt-history matches for ranking, using
|
|
27
|
+
* both signals rather than replacing one with the other.
|
|
28
|
+
*
|
|
29
|
+
* - `fuzzy` is the ordered fuzzy-filter result over session metadata (best first).
|
|
30
|
+
* - `historyIds` are session IDs whose recorded prompts matched the query,
|
|
31
|
+
* ordered by history relevance (best first); duplicates are tolerated.
|
|
32
|
+
*
|
|
33
|
+
* Ranking: sessions matched by **both** signals lead (keeping fuzzy order), then
|
|
34
|
+
* fuzzy-only matches, then history-only matches (by history order). A fuzzy match
|
|
35
|
+
* is never dropped, and history matches not present in `allSessions` (e.g. deleted
|
|
36
|
+
* or out-of-scope sessions) are ignored since they cannot be resumed from here.
|
|
37
|
+
*/
|
|
38
|
+
export function mergeSessionRanking(
|
|
39
|
+
allSessions: SessionInfo[],
|
|
40
|
+
fuzzy: SessionInfo[],
|
|
41
|
+
historyIds: string[],
|
|
42
|
+
): SessionInfo[] {
|
|
43
|
+
const historyRank = new Map<string, number>();
|
|
44
|
+
historyIds.forEach((id, index) => {
|
|
45
|
+
if (!historyRank.has(id)) historyRank.set(id, index);
|
|
46
|
+
});
|
|
47
|
+
if (historyRank.size === 0) return fuzzy;
|
|
48
|
+
|
|
49
|
+
const both: SessionInfo[] = [];
|
|
50
|
+
const fuzzyOnly: SessionInfo[] = [];
|
|
51
|
+
const fuzzyPaths = new Set<string>();
|
|
52
|
+
for (const session of fuzzy) {
|
|
53
|
+
fuzzyPaths.add(session.path);
|
|
54
|
+
(historyRank.has(session.id) ? both : fuzzyOnly).push(session);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const historyOnly = allSessions
|
|
58
|
+
.filter(session => historyRank.has(session.id) && !fuzzyPaths.has(session.path))
|
|
59
|
+
.sort((a, b) => (historyRank.get(a.id) ?? 0) - (historyRank.get(b.id) ?? 0));
|
|
60
|
+
|
|
61
|
+
return [...both, ...fuzzyOnly, ...historyOnly];
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/**
|
|
22
65
|
* Custom session list component with multi-line items and search
|
|
23
66
|
*/
|
|
@@ -25,33 +68,44 @@ class SessionList implements Component {
|
|
|
25
68
|
#filteredSessions: SessionInfo[] = [];
|
|
26
69
|
#selectedIndex: number = 0;
|
|
27
70
|
readonly #searchInput: Input;
|
|
28
|
-
onSelect?: (
|
|
71
|
+
onSelect?: (session: SessionInfo) => void;
|
|
29
72
|
onCancel?: () => void;
|
|
30
73
|
onExit: () => void = () => {};
|
|
74
|
+
onToggleScope?: () => void;
|
|
31
75
|
#maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
|
|
32
76
|
|
|
33
77
|
onDeleteRequest?: (session: SessionInfo) => void;
|
|
34
78
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
79
|
+
#allSessions: SessionInfo[];
|
|
80
|
+
#showCwd: boolean;
|
|
81
|
+
readonly #historyMatcher?: SessionHistoryMatcher;
|
|
82
|
+
|
|
83
|
+
constructor(sessions: SessionInfo[], showCwd = false, historyMatcher?: SessionHistoryMatcher) {
|
|
84
|
+
this.#allSessions = sessions;
|
|
85
|
+
this.#showCwd = showCwd;
|
|
86
|
+
this.#historyMatcher = historyMatcher;
|
|
87
|
+
this.#filteredSessions = sessions;
|
|
40
88
|
this.#searchInput = new Input();
|
|
41
89
|
|
|
42
90
|
// Handle Enter in search input - select current item
|
|
43
91
|
this.#searchInput.onSubmit = () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.onSelect(selected.path);
|
|
48
|
-
}
|
|
92
|
+
const selected = this.#filteredSessions[this.#selectedIndex];
|
|
93
|
+
if (selected) {
|
|
94
|
+
this.onSelect?.(selected);
|
|
49
95
|
}
|
|
50
96
|
};
|
|
51
97
|
}
|
|
52
98
|
|
|
99
|
+
/** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
|
|
100
|
+
setSessions(sessions: SessionInfo[], showCwd: boolean): void {
|
|
101
|
+
this.#allSessions = sessions;
|
|
102
|
+
this.#showCwd = showCwd;
|
|
103
|
+
this.#selectedIndex = 0;
|
|
104
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
105
|
+
}
|
|
106
|
+
|
|
53
107
|
#filterSessions(query: string): void {
|
|
54
|
-
|
|
108
|
+
const fuzzy = fuzzyFilter(this.#allSessions, query, session => {
|
|
55
109
|
const parts = [
|
|
56
110
|
session.id,
|
|
57
111
|
session.title ?? "",
|
|
@@ -62,13 +116,28 @@ class SessionList implements Component {
|
|
|
62
116
|
];
|
|
63
117
|
return parts.filter(Boolean).join(" ");
|
|
64
118
|
});
|
|
119
|
+
this.#filteredSessions = this.#mergeHistoryMatches(query, fuzzy);
|
|
65
120
|
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, this.#filteredSessions.length - 1));
|
|
66
121
|
}
|
|
67
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Augment fuzzy results with prompt-history matches without replacing them.
|
|
125
|
+
* The session-list corpus only sees the first 4KB of each session, so a prompt
|
|
126
|
+
* typed deep into a long session is invisible to fuzzy search; `historyMatcher`
|
|
127
|
+
* recovers those via `history.db`.
|
|
128
|
+
*/
|
|
129
|
+
#mergeHistoryMatches(query: string, fuzzy: SessionInfo[]): SessionInfo[] {
|
|
130
|
+
const trimmed = query.trim();
|
|
131
|
+
if (!trimmed || !this.#historyMatcher) return fuzzy;
|
|
132
|
+
const historyIds = this.#historyMatcher(trimmed);
|
|
133
|
+
if (historyIds.length === 0) return fuzzy;
|
|
134
|
+
return mergeSessionRanking(this.#allSessions, fuzzy, historyIds);
|
|
135
|
+
}
|
|
136
|
+
|
|
68
137
|
removeSession(sessionPath: string): void {
|
|
69
|
-
const index = this
|
|
138
|
+
const index = this.#allSessions.findIndex(s => s.path === sessionPath);
|
|
70
139
|
if (index === -1) return;
|
|
71
|
-
this
|
|
140
|
+
this.#allSessions.splice(index, 1);
|
|
72
141
|
// Re-filter to update filteredSessions
|
|
73
142
|
this.#filterSessions(this.#searchInput.getValue());
|
|
74
143
|
// Adjust selectedIndex if we deleted the last item or beyond
|
|
@@ -89,7 +158,7 @@ class SessionList implements Component {
|
|
|
89
158
|
lines.push(""); // Blank line after search
|
|
90
159
|
|
|
91
160
|
if (this.#filteredSessions.length === 0) {
|
|
92
|
-
if (this
|
|
161
|
+
if (this.#showCwd) {
|
|
93
162
|
// "All" scope - no sessions anywhere that match filter
|
|
94
163
|
lines.push(truncateToWidth(theme.fg("muted", " No sessions found"), width));
|
|
95
164
|
} else {
|
|
@@ -158,9 +227,12 @@ class SessionList implements Component {
|
|
|
158
227
|
lines.push(messageLine);
|
|
159
228
|
}
|
|
160
229
|
|
|
161
|
-
// Metadata line: date + file size
|
|
230
|
+
// Metadata line: date + file size (+ project dir in all-projects scope)
|
|
162
231
|
const modified = formatDate(session.modified);
|
|
163
|
-
|
|
232
|
+
let metadata = ` ${modified} ${theme.sep.dot} ${formatBytes(session.size)}`;
|
|
233
|
+
if (this.#showCwd && session.cwd) {
|
|
234
|
+
metadata += ` ${theme.sep.dot} ${shortenPath(session.cwd)}`;
|
|
235
|
+
}
|
|
164
236
|
const metadataLine = theme.fg("dim", truncateToWidth(metadata, width));
|
|
165
237
|
|
|
166
238
|
lines.push(metadataLine);
|
|
@@ -176,7 +248,12 @@ class SessionList implements Component {
|
|
|
176
248
|
|
|
177
249
|
// Add keybinding hint
|
|
178
250
|
lines.push("");
|
|
179
|
-
lines.push(
|
|
251
|
+
lines.push(
|
|
252
|
+
theme.fg(
|
|
253
|
+
"muted",
|
|
254
|
+
` [Del delete · Enter select · Tab ${this.#showCwd ? "current folder" : "all projects"} · Esc cancel]`,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
180
257
|
|
|
181
258
|
return lines;
|
|
182
259
|
}
|
|
@@ -215,7 +292,7 @@ class SessionList implements Component {
|
|
|
215
292
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
216
293
|
const selected = this.#filteredSessions[this.#selectedIndex];
|
|
217
294
|
if (selected && this.onSelect) {
|
|
218
|
-
this.onSelect(selected
|
|
295
|
+
this.onSelect(selected);
|
|
219
296
|
}
|
|
220
297
|
return;
|
|
221
298
|
}
|
|
@@ -231,12 +308,28 @@ class SessionList implements Component {
|
|
|
231
308
|
this.onExit();
|
|
232
309
|
return;
|
|
233
310
|
}
|
|
311
|
+
// Tab - toggle folder / all-projects scope
|
|
312
|
+
if (matchesKey(keyData, "tab")) {
|
|
313
|
+
this.onToggleScope?.();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
234
316
|
// Pass everything else to search input
|
|
235
317
|
this.#searchInput.handleInput(keyData);
|
|
236
318
|
this.#filterSessions(this.#searchInput.getValue());
|
|
237
319
|
}
|
|
238
320
|
}
|
|
239
321
|
|
|
322
|
+
export interface SessionSelectorOptions {
|
|
323
|
+
onDelete?: (session: SessionInfo) => Promise<boolean>;
|
|
324
|
+
historyMatcher?: SessionHistoryMatcher;
|
|
325
|
+
/** Loads sessions across all projects for the all-projects scope toggle (Tab). */
|
|
326
|
+
loadAllSessions?: () => Promise<SessionInfo[]>;
|
|
327
|
+
/** Preloaded all-projects list; cached so the first Tab toggle is instant. */
|
|
328
|
+
allSessions?: SessionInfo[];
|
|
329
|
+
/** Open directly in all-projects scope (e.g. the current folder has no sessions). */
|
|
330
|
+
startInAllScope?: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
240
333
|
/**
|
|
241
334
|
* Component that renders a session selector with optional confirmation dialog
|
|
242
335
|
*/
|
|
@@ -244,35 +337,55 @@ export class SessionSelectorComponent extends Container {
|
|
|
244
337
|
#sessionList: SessionList;
|
|
245
338
|
#confirmationDialog: HookSelectorComponent | null = null;
|
|
246
339
|
#messageContainer: Container;
|
|
340
|
+
#headerText: Text;
|
|
247
341
|
#onDelete?: (session: SessionInfo) => Promise<boolean>;
|
|
248
342
|
#onRequestRender?: () => void;
|
|
343
|
+
readonly #loadAllSessions?: () => Promise<SessionInfo[]>;
|
|
344
|
+
#folderSessions: SessionInfo[];
|
|
345
|
+
#globalSessions: SessionInfo[] | null = null;
|
|
346
|
+
#scope: "folder" | "all" = "folder";
|
|
347
|
+
#toggling = false;
|
|
249
348
|
|
|
250
349
|
constructor(
|
|
251
350
|
sessions: SessionInfo[],
|
|
252
|
-
onSelect: (
|
|
351
|
+
onSelect: (session: SessionInfo) => void,
|
|
253
352
|
onCancel: () => void,
|
|
254
353
|
onExit: () => void,
|
|
255
|
-
|
|
354
|
+
options: SessionSelectorOptions = {},
|
|
256
355
|
) {
|
|
257
356
|
super();
|
|
258
357
|
|
|
259
358
|
this.#messageContainer = new Container();
|
|
260
|
-
this.#onDelete = onDelete;
|
|
359
|
+
this.#onDelete = options.onDelete;
|
|
360
|
+
this.#loadAllSessions = options.loadAllSessions;
|
|
361
|
+
this.#folderSessions = sessions;
|
|
362
|
+
this.#globalSessions = options.allSessions ?? null;
|
|
363
|
+
// Open in all-projects scope when asked and we already have that list
|
|
364
|
+
// (e.g. the current folder has no sessions to show).
|
|
365
|
+
const startAll = options.startInAllScope === true && this.#globalSessions !== null;
|
|
366
|
+
this.#scope = startAll ? "all" : "folder";
|
|
367
|
+
const initialSessions = startAll ? this.#globalSessions! : sessions;
|
|
261
368
|
// Add header
|
|
262
369
|
this.addChild(new Spacer(1));
|
|
263
|
-
this
|
|
370
|
+
this.#headerText = new Text(this.#headerLabel(), 1, 0);
|
|
371
|
+
this.addChild(this.#headerText);
|
|
264
372
|
this.addChild(new Spacer(1));
|
|
265
373
|
this.addChild(new DynamicBorder());
|
|
266
374
|
this.addChild(new Spacer(1));
|
|
267
375
|
this.addChild(this.#messageContainer);
|
|
268
376
|
// Create session list
|
|
269
|
-
this.#sessionList = new SessionList(
|
|
377
|
+
this.#sessionList = new SessionList(initialSessions, startAll, options.historyMatcher);
|
|
270
378
|
this.#sessionList.onSelect = onSelect;
|
|
271
379
|
this.#sessionList.onCancel = onCancel;
|
|
272
380
|
this.#sessionList.onExit = onExit;
|
|
273
381
|
this.#sessionList.onDeleteRequest = (session: SessionInfo) => {
|
|
274
382
|
this.#showDeleteConfirmation(session);
|
|
275
383
|
};
|
|
384
|
+
if (this.#loadAllSessions || this.#globalSessions) {
|
|
385
|
+
this.#sessionList.onToggleScope = () => {
|
|
386
|
+
void this.#toggleScope();
|
|
387
|
+
};
|
|
388
|
+
}
|
|
276
389
|
this.addChild(this.#sessionList);
|
|
277
390
|
|
|
278
391
|
// Add bottom border
|
|
@@ -280,6 +393,48 @@ export class SessionSelectorComponent extends Container {
|
|
|
280
393
|
this.addChild(new DynamicBorder());
|
|
281
394
|
}
|
|
282
395
|
|
|
396
|
+
#headerLabel(): string {
|
|
397
|
+
const scopeLabel = this.#scope === "all" ? "all projects" : "current folder";
|
|
398
|
+
return `${theme.bold("Resume Session")} ${theme.fg("muted", `(${scopeLabel})`)}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Toggle between current-folder and all-projects scope. The global list is
|
|
403
|
+
* loaded lazily on first switch and cached, so the common folder-scope path
|
|
404
|
+
* never pays for the cross-project scan.
|
|
405
|
+
*/
|
|
406
|
+
async #toggleScope(): Promise<void> {
|
|
407
|
+
if (this.#toggling || this.#confirmationDialog) return;
|
|
408
|
+
if (this.#scope === "folder") {
|
|
409
|
+
let global = this.#globalSessions;
|
|
410
|
+
if (!global) {
|
|
411
|
+
if (!this.#loadAllSessions) return;
|
|
412
|
+
this.#toggling = true;
|
|
413
|
+
this.#messageContainer.clear();
|
|
414
|
+
this.#messageContainer.addChild(new Text(theme.fg("muted", " Loading all projects…"), 1, 0));
|
|
415
|
+
this.#onRequestRender?.();
|
|
416
|
+
try {
|
|
417
|
+
global = await this.#loadAllSessions();
|
|
418
|
+
} catch (err) {
|
|
419
|
+
this.#showError(err instanceof Error ? err.message : String(err));
|
|
420
|
+
this.#toggling = false;
|
|
421
|
+
this.#onRequestRender?.();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.#globalSessions = global;
|
|
425
|
+
this.#messageContainer.clear();
|
|
426
|
+
this.#toggling = false;
|
|
427
|
+
}
|
|
428
|
+
this.#scope = "all";
|
|
429
|
+
this.#sessionList.setSessions(global, true);
|
|
430
|
+
} else {
|
|
431
|
+
this.#scope = "folder";
|
|
432
|
+
this.#sessionList.setSessions(this.#folderSessions, false);
|
|
433
|
+
}
|
|
434
|
+
this.#headerText.setText(this.#headerLabel());
|
|
435
|
+
this.#onRequestRender?.();
|
|
436
|
+
}
|
|
437
|
+
|
|
283
438
|
setOnRequestRender(callback: () => void): void {
|
|
284
439
|
this.#onRequestRender = callback;
|
|
285
440
|
}
|
|
@@ -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
|
*/
|
|
@@ -172,11 +180,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
172
180
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
173
181
|
#editDiffAbort?: AbortController;
|
|
174
182
|
#editDiffLastArgsKey?: string;
|
|
183
|
+
// Latest in-flight streaming diff recompute, captured so it can be awaited.
|
|
184
|
+
#editDiffInFlight?: Promise<void>;
|
|
175
185
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
176
186
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
177
187
|
// Spinner animation for partial task results
|
|
178
188
|
#spinnerFrame?: number;
|
|
179
189
|
#spinnerInterval?: NodeJS.Timeout;
|
|
190
|
+
#lastSpinnerAdvanceAt = 0;
|
|
180
191
|
// Todo write completion strikethrough reveal animation
|
|
181
192
|
#todoStrikeInterval?: NodeJS.Timeout;
|
|
182
193
|
// Track if args are still being streamed (for edit/write spinner)
|
|
@@ -230,7 +241,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
230
241
|
this.#editMode = resolveEditModeForTool(toolName, tool);
|
|
231
242
|
|
|
232
243
|
this.#updateDisplay();
|
|
233
|
-
|
|
244
|
+
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
updateArgs(args: any, _toolCallId?: string): void {
|
|
@@ -241,7 +252,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
241
252
|
if (args === this.#args) return;
|
|
242
253
|
this.#args = args;
|
|
243
254
|
this.#updateSpinnerAnimation();
|
|
244
|
-
|
|
255
|
+
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
245
256
|
this.#updateDisplay();
|
|
246
257
|
}
|
|
247
258
|
|
|
@@ -252,7 +263,18 @@ export class ToolExecutionComponent extends Container {
|
|
|
252
263
|
setArgsComplete(_toolCallId?: string): void {
|
|
253
264
|
this.#argsComplete = true;
|
|
254
265
|
this.#updateSpinnerAnimation();
|
|
255
|
-
|
|
266
|
+
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Await the streaming diff recompute kicked off by the most recent
|
|
271
|
+
* `updateArgs`/`setArgsComplete`. The recompute reads the file and re-runs the
|
|
272
|
+
* whole-file Myers diff off the render path, signalling completion only via a
|
|
273
|
+
* throttled `requestRender`. Tests await this to sample a *settled* preview
|
|
274
|
+
* deterministically instead of racing the spinner's render ticks.
|
|
275
|
+
*/
|
|
276
|
+
async whenPreviewSettled(): Promise<void> {
|
|
277
|
+
await this.#editDiffInFlight;
|
|
256
278
|
}
|
|
257
279
|
|
|
258
280
|
async #runPreviewDiff(): Promise<void> {
|
|
@@ -404,13 +426,20 @@ export class ToolExecutionComponent extends Container {
|
|
|
404
426
|
this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
|
|
405
427
|
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
406
428
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
429
|
+
this.#lastSpinnerAdvanceAt = performance.now();
|
|
407
430
|
this.#spinnerInterval = setInterval(() => {
|
|
431
|
+
const now = performance.now();
|
|
408
432
|
const frameCount = theme.spinnerFrames.length;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
433
|
+
// Redraw at ~60fps for a smooth border sweep, but only step the spinner
|
|
434
|
+
// glyph at its classic ~12.5fps cadence. The TUI throttles renders at
|
|
435
|
+
// 16ms and the differ drops no-op redraws, so the extra ticks are free.
|
|
436
|
+
if (frameCount > 0 && now - this.#lastSpinnerAdvanceAt >= SPINNER_GLYPH_ADVANCE_MS) {
|
|
437
|
+
this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
|
|
438
|
+
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
439
|
+
this.#lastSpinnerAdvanceAt = now;
|
|
440
|
+
}
|
|
412
441
|
this.#ui.requestRender();
|
|
413
|
-
},
|
|
442
|
+
}, SPINNER_RENDER_INTERVAL_MS);
|
|
414
443
|
} else if (!needsSpinner && this.#spinnerInterval) {
|
|
415
444
|
clearInterval(this.#spinnerInterval);
|
|
416
445
|
this.#spinnerInterval = undefined;
|
|
@@ -11,10 +11,8 @@ import {
|
|
|
11
11
|
type UsageReport,
|
|
12
12
|
} from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
14
|
-
import { formatDuration, Snowflake
|
|
14
|
+
import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { $ } from "bun";
|
|
16
|
-
import { reset as resetCapabilities } from "../../capability";
|
|
17
|
-
import { clearClaudePluginRootsCache } from "../../discovery/helpers";
|
|
18
16
|
import { loadCustomShare } from "../../export/custom-share";
|
|
19
17
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
20
18
|
import {
|
|
@@ -993,14 +991,7 @@ export class CommandController {
|
|
|
993
991
|
try {
|
|
994
992
|
await this.ctx.sessionManager.flush();
|
|
995
993
|
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
996
|
-
|
|
997
|
-
clearClaudePluginRootsCache(); // re-warms preloadedPluginRoots with new project dir (async)
|
|
998
|
-
resetCapabilities();
|
|
999
|
-
await this.ctx.refreshSlashCommandState(resolvedPath);
|
|
1000
|
-
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
|
|
1001
|
-
|
|
1002
|
-
this.ctx.statusLine.invalidate();
|
|
1003
|
-
this.ctx.updateEditorTopBorder();
|
|
994
|
+
await this.ctx.applyCwdChange(resolvedPath);
|
|
1004
995
|
|
|
1005
996
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
1006
997
|
this.ctx.chatContainer.addChild(
|
|
@@ -277,8 +277,11 @@ export class EventController {
|
|
|
277
277
|
this.#lastThinkingCount = 0;
|
|
278
278
|
this.#assistantMessageStreaming = true;
|
|
279
279
|
this.#resetReadGroup();
|
|
280
|
-
this.ctx.streamingComponent = new AssistantMessageComponent(
|
|
281
|
-
|
|
280
|
+
this.ctx.streamingComponent = new AssistantMessageComponent(
|
|
281
|
+
undefined,
|
|
282
|
+
this.ctx.hideThinkingBlock,
|
|
283
|
+
() => this.ctx.ui.requestRender(),
|
|
284
|
+
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
282
285
|
);
|
|
283
286
|
this.ctx.streamingMessage = event.message;
|
|
284
287
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -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,
|
|
@@ -21,7 +22,7 @@ import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
|
21
22
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
22
23
|
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
23
24
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
24
|
-
import type { InteractiveModeContext } from "../../modes/types";
|
|
25
|
+
import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
|
|
25
26
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
26
27
|
|
|
27
28
|
const MAX_WIDGET_LINES = 10;
|
|
@@ -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,8 +582,8 @@ export class ExtensionUiController {
|
|
|
581
582
|
*/
|
|
582
583
|
showHookSelector(
|
|
583
584
|
title: string,
|
|
584
|
-
options:
|
|
585
|
-
dialogOptions?:
|
|
585
|
+
options: ExtensionUISelectItem[],
|
|
586
|
+
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
586
587
|
extra?: { slider?: HookSelectorSlider },
|
|
587
588
|
): Promise<string | undefined> {
|
|
588
589
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
@@ -623,6 +624,7 @@ export class ExtensionUiController {
|
|
|
623
624
|
onTimeout: dialogOptions?.onTimeout,
|
|
624
625
|
tui: this.ctx.ui,
|
|
625
626
|
outline: dialogOptions?.outline,
|
|
627
|
+
disabledIndices: dialogOptions?.disabledIndices,
|
|
626
628
|
maxVisible,
|
|
627
629
|
slider: extra?.slider,
|
|
628
630
|
},
|
|
@@ -15,7 +15,7 @@ import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registr
|
|
|
15
15
|
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
16
16
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
17
17
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
18
|
-
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
18
|
+
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
19
19
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
20
20
|
import { ensureSupportedImageInput } from "../../utils/image-loading";
|
|
21
21
|
import { resizeImage } from "../../utils/image-resize";
|
|
@@ -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();
|
|
@@ -188,6 +173,11 @@ export class InputController {
|
|
|
188
173
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
189
174
|
);
|
|
190
175
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
176
|
+
this.ctx.editor.setActionKeys(
|
|
177
|
+
"app.clipboard.pasteTextRaw",
|
|
178
|
+
this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
|
|
179
|
+
);
|
|
180
|
+
this.ctx.editor.onPasteTextRaw = () => void this.handleClipboardTextRawPaste();
|
|
191
181
|
this.ctx.editor.setActionKeys(
|
|
192
182
|
"app.clipboard.copyPrompt",
|
|
193
183
|
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
@@ -705,6 +695,19 @@ export class InputController {
|
|
|
705
695
|
}
|
|
706
696
|
}
|
|
707
697
|
|
|
698
|
+
async handleClipboardTextRawPaste(): Promise<void> {
|
|
699
|
+
try {
|
|
700
|
+
const text = await readTextFromClipboard();
|
|
701
|
+
if (text) {
|
|
702
|
+
this.ctx.editor.insertText(text);
|
|
703
|
+
this.ctx.ui.requestRender();
|
|
704
|
+
this.ctx.showStatus("No text in clipboard to paste raw");
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
this.ctx.showStatus("Failed to paste raw text from clipboard");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
708
711
|
createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
|
|
709
712
|
return createPromptActionAutocompleteProvider({
|
|
710
713
|
commands,
|