@oh-my-pi/pi-coding-agent 15.7.6 → 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 +146 -198
- 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 +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- 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 +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- 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 +4 -2
- 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/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- 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/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/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- 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 +11 -10
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- 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 +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.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/eval.md +5 -5
- 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 +62 -46
- 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/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- 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
|
}
|
|
@@ -180,6 +180,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
180
180
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
181
181
|
#editDiffAbort?: AbortController;
|
|
182
182
|
#editDiffLastArgsKey?: string;
|
|
183
|
+
// Latest in-flight streaming diff recompute, captured so it can be awaited.
|
|
184
|
+
#editDiffInFlight?: Promise<void>;
|
|
183
185
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
184
186
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
185
187
|
// Spinner animation for partial task results
|
|
@@ -239,7 +241,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
239
241
|
this.#editMode = resolveEditModeForTool(toolName, tool);
|
|
240
242
|
|
|
241
243
|
this.#updateDisplay();
|
|
242
|
-
|
|
244
|
+
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
updateArgs(args: any, _toolCallId?: string): void {
|
|
@@ -250,7 +252,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
250
252
|
if (args === this.#args) return;
|
|
251
253
|
this.#args = args;
|
|
252
254
|
this.#updateSpinnerAnimation();
|
|
253
|
-
|
|
255
|
+
this.#editDiffInFlight = this.#runPreviewDiff();
|
|
254
256
|
this.#updateDisplay();
|
|
255
257
|
}
|
|
256
258
|
|
|
@@ -261,7 +263,18 @@ export class ToolExecutionComponent extends Container {
|
|
|
261
263
|
setArgsComplete(_toolCallId?: string): void {
|
|
262
264
|
this.#argsComplete = true;
|
|
263
265
|
this.#updateSpinnerAnimation();
|
|
264
|
-
|
|
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;
|
|
265
278
|
}
|
|
266
279
|
|
|
267
280
|
async #runPreviewDiff(): Promise<void> {
|
|
@@ -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(
|
|
@@ -22,7 +22,7 @@ import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
|
22
22
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
23
23
|
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
24
24
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
25
|
-
import type { InteractiveModeContext } from "../../modes/types";
|
|
25
|
+
import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
|
|
26
26
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
27
27
|
|
|
28
28
|
const MAX_WIDGET_LINES = 10;
|
|
@@ -583,7 +583,7 @@ export class ExtensionUiController {
|
|
|
583
583
|
showHookSelector(
|
|
584
584
|
title: string,
|
|
585
585
|
options: ExtensionUISelectItem[],
|
|
586
|
-
dialogOptions?:
|
|
586
|
+
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
587
587
|
extra?: { slider?: HookSelectorSlider },
|
|
588
588
|
): Promise<string | undefined> {
|
|
589
589
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
@@ -624,6 +624,7 @@ export class ExtensionUiController {
|
|
|
624
624
|
onTimeout: dialogOptions?.onTimeout,
|
|
625
625
|
tui: this.ctx.ui,
|
|
626
626
|
outline: dialogOptions?.outline,
|
|
627
|
+
disabledIndices: dialogOptions?.disabledIndices,
|
|
627
628
|
maxVisible,
|
|
628
629
|
slider: extra?.slider,
|
|
629
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";
|
|
@@ -173,6 +173,11 @@ export class InputController {
|
|
|
173
173
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
174
174
|
);
|
|
175
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();
|
|
176
181
|
this.ctx.editor.setActionKeys(
|
|
177
182
|
"app.clipboard.copyPrompt",
|
|
178
183
|
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
@@ -690,6 +695,19 @@ export class InputController {
|
|
|
690
695
|
}
|
|
691
696
|
}
|
|
692
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
|
+
|
|
693
711
|
createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
|
|
694
712
|
return createPromptActionAutocompleteProvider({
|
|
695
713
|
commands,
|
|
@@ -3,7 +3,7 @@ import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
|
3
3
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
4
4
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { getRoleInfo } from "../../config/model-registry";
|
|
8
8
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
9
9
|
import { settings } from "../../config/settings";
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
setPreferredImageProvider,
|
|
37
37
|
setPreferredSearchProvider,
|
|
38
38
|
} from "../../tools";
|
|
39
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
39
40
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
40
41
|
import { AgentDashboard } from "../components/agent-dashboard";
|
|
41
42
|
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
@@ -719,12 +720,22 @@ export class SelectorController {
|
|
|
719
720
|
this.ctx.sessionManager.getCwd(),
|
|
720
721
|
this.ctx.sessionManager.getSessionDir(),
|
|
721
722
|
);
|
|
723
|
+
// Current folder has no sessions: preload the global list so the picker
|
|
724
|
+
// can open straight into all-projects scope instead of dead-ending.
|
|
725
|
+
let allSessions: SessionInfo[] | undefined;
|
|
726
|
+
let startInAllScope = false;
|
|
727
|
+
if (sessions.length === 0) {
|
|
728
|
+
allSessions = await SessionManager.listAll();
|
|
729
|
+
startInAllScope = allSessions.length > 0;
|
|
730
|
+
}
|
|
731
|
+
const historyStorage = this.ctx.historyStorage;
|
|
732
|
+
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
722
733
|
this.showSelector(done => {
|
|
723
734
|
const selector = new SessionSelectorComponent(
|
|
724
735
|
sessions,
|
|
725
|
-
async
|
|
736
|
+
async (session: SessionInfo) => {
|
|
726
737
|
done();
|
|
727
|
-
await this.handleResumeSession(
|
|
738
|
+
await this.handleResumeSession(session.path);
|
|
728
739
|
},
|
|
729
740
|
() => {
|
|
730
741
|
done();
|
|
@@ -733,19 +744,25 @@ export class SelectorController {
|
|
|
733
744
|
() => {
|
|
734
745
|
void this.ctx.shutdown();
|
|
735
746
|
},
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
747
|
+
{
|
|
748
|
+
onDelete: async (session: SessionInfo) => {
|
|
749
|
+
if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
const storage = new FileSessionStorage();
|
|
753
|
+
try {
|
|
754
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
755
|
+
return true;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
|
|
758
|
+
cause: err,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
historyMatcher,
|
|
763
|
+
loadAllSessions: () => SessionManager.listAll(),
|
|
764
|
+
allSessions,
|
|
765
|
+
startInAllScope,
|
|
749
766
|
},
|
|
750
767
|
);
|
|
751
768
|
selector.setOnRequestRender(() => this.ctx.ui.requestRender());
|
|
@@ -801,8 +818,17 @@ export class SelectorController {
|
|
|
801
818
|
async handleResumeSession(sessionPath: string): Promise<void> {
|
|
802
819
|
this.#clearTransientSessionUi();
|
|
803
820
|
|
|
804
|
-
|
|
821
|
+
const previousCwd = this.ctx.sessionManager.getCwd();
|
|
822
|
+
// Switch session via AgentSession (emits hook and tool session events). The
|
|
823
|
+
// SessionManager adopts the resumed session's own cwd when it differs.
|
|
805
824
|
await this.ctx.session.switchSession(sessionPath);
|
|
825
|
+
const newCwd = this.ctx.sessionManager.getCwd();
|
|
826
|
+
const movedProject = normalizePathForComparison(newCwd) !== normalizePathForComparison(previousCwd);
|
|
827
|
+
if (movedProject) {
|
|
828
|
+
// Resumed a session from another project: re-point the process and every
|
|
829
|
+
// cwd-derived cache at it before rendering.
|
|
830
|
+
await this.ctx.applyCwdChange(newCwd);
|
|
831
|
+
}
|
|
806
832
|
this.#refreshSessionTerminalTitle();
|
|
807
833
|
this.ctx.updateEditorBorderColor();
|
|
808
834
|
|
|
@@ -810,7 +836,7 @@ export class SelectorController {
|
|
|
810
836
|
this.ctx.chatContainer.clear();
|
|
811
837
|
this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
|
|
812
838
|
await this.ctx.reloadTodos();
|
|
813
|
-
this.ctx.showStatus("Resumed session");
|
|
839
|
+
this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
|
|
814
840
|
}
|
|
815
841
|
|
|
816
842
|
async handleSessionDeleteCommand(): Promise<void> {
|
|
@@ -919,7 +945,15 @@ export class SelectorController {
|
|
|
919
945
|
|
|
920
946
|
async #handleOAuthLogout(providerId: string): Promise<void> {
|
|
921
947
|
try {
|
|
922
|
-
|
|
948
|
+
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
949
|
+
if (!authStorage.has(providerId)) {
|
|
950
|
+
const source = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
951
|
+
const suffix = source ? ` Current auth comes from ${source}; remove that source to log out.` : "";
|
|
952
|
+
this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
await authStorage.logout(providerId);
|
|
923
957
|
await this.ctx.session.modelRegistry.refresh();
|
|
924
958
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
925
959
|
this.ctx.chatContainer.addChild(
|
|
@@ -928,6 +962,12 @@ export class SelectorController {
|
|
|
928
962
|
this.ctx.chatContainer.addChild(
|
|
929
963
|
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
|
|
930
964
|
);
|
|
965
|
+
const remainingSource = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
966
|
+
if (remainingSource) {
|
|
967
|
+
this.ctx.chatContainer.addChild(
|
|
968
|
+
new Text(theme.fg("warning", `${providerId} is still authenticated via ${remainingSource}`), 1, 0),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
931
971
|
this.ctx.ui.requestRender();
|
|
932
972
|
} catch (error: unknown) {
|
|
933
973
|
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -948,10 +988,10 @@ export class SelectorController {
|
|
|
948
988
|
await this.#refreshOAuthProviderAuthState();
|
|
949
989
|
const oauthProviders = getOAuthProviders();
|
|
950
990
|
const loggedInProviders = oauthProviders.filter(provider =>
|
|
951
|
-
this.ctx.session.modelRegistry.authStorage.
|
|
991
|
+
this.ctx.session.modelRegistry.authStorage.has(provider.id),
|
|
952
992
|
);
|
|
953
993
|
if (loggedInProviders.length === 0) {
|
|
954
|
-
this.ctx.showStatus("No
|
|
994
|
+
this.ctx.showStatus("No stored provider credentials to log out. Remove env or config auth at its source.");
|
|
955
995
|
return;
|
|
956
996
|
}
|
|
957
997
|
}
|