@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4
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 +63 -0
- package/dist/cli.js +3806 -3798
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/auto-generated-guard.d.ts +5 -2
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/memory-render.d.ts +1 -4
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/edit/hashline/filesystem.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/stream-guards.ts +1 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/auto-generated-guard.ts +18 -5
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/memory-render.ts +11 -2
- package/src/tools/render-utils.ts +8 -3
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
- package/src/tools/write.ts +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CustomEditor,
|
|
9
9
|
extractBracketedImagePastePaths,
|
|
10
10
|
extractBracketedPastePaths,
|
|
11
|
+
extractImagePastePathsFromText,
|
|
11
12
|
extractImagePathFromText,
|
|
12
13
|
extractPastePathsFromText,
|
|
13
14
|
SPACE_HOLD_MECHANICAL_RUN,
|
|
@@ -182,6 +183,43 @@ describe("CustomEditor bracketed path paste", () => {
|
|
|
182
183
|
expect(editor.getText()).toBe("/tmp/report.csv");
|
|
183
184
|
expect(imagePathCalls).toBe(0);
|
|
184
185
|
});
|
|
186
|
+
|
|
187
|
+
it("attaches a spaced screenshot path as an image instead of inserting it as literal text", () => {
|
|
188
|
+
// #6578: the raw macOS "copy screenshot path" payload has unescaped
|
|
189
|
+
// spaces, which defeats the segment splitter. Before the whole-text
|
|
190
|
+
// fallback the paste degraded to literal text in the prompt.
|
|
191
|
+
const { editor } = makeEditor();
|
|
192
|
+
const screenshot = "/Users/me/Desktop/Screenshot 2026-07-24 at 1.55.12 PM.png";
|
|
193
|
+
const pasted: string[] = [];
|
|
194
|
+
editor.onPasteImagePath = path => {
|
|
195
|
+
pasted.push(path);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
editor.handleInput(bracketedPaste(screenshot));
|
|
199
|
+
|
|
200
|
+
expect(pasted).toEqual([screenshot]);
|
|
201
|
+
expect(editor.getText()).toBe("");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("keeps a two-file drag with unescaped spaces as text instead of attaching one fused path", () => {
|
|
205
|
+
// PR #6582 review: selecting two screenshots and dropping them together
|
|
206
|
+
// emits a single space-separated payload the splitter also refuses
|
|
207
|
+
// (`PM.png` carries no directory). Fusing it into one path attaches
|
|
208
|
+
// nothing — `handleImagePathPaste` hits ENOENT and only shows a status,
|
|
209
|
+
// never restoring the text — so the drop must degrade to a text paste.
|
|
210
|
+
const { editor } = makeEditor();
|
|
211
|
+
const dropped =
|
|
212
|
+
"/Users/me/Desktop/Screenshot 2026-07-24 at 1.55.12 PM.png /Users/me/Desktop/Screenshot 2026-07-24 at 1.56.00 PM.png";
|
|
213
|
+
const pasted: string[] = [];
|
|
214
|
+
editor.onPasteImagePath = path => {
|
|
215
|
+
pasted.push(path);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
editor.handleInput(bracketedPaste(dropped));
|
|
219
|
+
|
|
220
|
+
expect(pasted).toEqual([]);
|
|
221
|
+
expect(editor.getText()).toBe(dropped);
|
|
222
|
+
});
|
|
185
223
|
});
|
|
186
224
|
describe("CustomEditor configured paste image keys", () => {
|
|
187
225
|
it("routes Ghostty Cmd+V kitty key events through the macOS image-paste default", () => {
|
|
@@ -250,6 +288,12 @@ describe("extractImagePathFromText (issue #3506)", () => {
|
|
|
250
288
|
);
|
|
251
289
|
});
|
|
252
290
|
|
|
291
|
+
it("returns undefined for two spaced paths the splitter could not separate", () => {
|
|
292
|
+
// Only the whole-text pass survives the splitter here, and it must not
|
|
293
|
+
// fuse the pair into one path the loader can never resolve.
|
|
294
|
+
expect(extractImagePathFromText("/tmp/a.png /tmp/b shot.png")).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
253
297
|
it("does not hijack prose that happens to contain a path-shaped fragment", () => {
|
|
254
298
|
// The whole-text branch is gated on ABSOLUTE_PATH_PREFIX_REGEX, so a
|
|
255
299
|
// non-anchored prefix ("see ...") never triggers it.
|
|
@@ -264,6 +308,132 @@ describe("extractPastePathsFromText", () => {
|
|
|
264
308
|
});
|
|
265
309
|
});
|
|
266
310
|
|
|
311
|
+
describe("extractImagePastePathsFromText (issue #6578)", () => {
|
|
312
|
+
const MAC_SCREENSHOT =
|
|
313
|
+
"/var/folders/xx/T/TemporaryItems/NSIRD_screencaptureui_ab/Screenshot 2026-07-24 at 1.55.12 PM.png";
|
|
314
|
+
const WINDOWS_SPACED = "C:\\Users\\me\\My Pictures\\shot 1.png";
|
|
315
|
+
|
|
316
|
+
// Every case must resolve identically on the stripped-marker route
|
|
317
|
+
// (assembled pastes) and the bracketed route, since the latter now
|
|
318
|
+
// delegates to the former.
|
|
319
|
+
const cases: { name: string; text: string; expected: string[] | undefined }[] = [
|
|
320
|
+
{ name: "a macOS screenshot path with unescaped spaces", text: MAC_SCREENSHOT, expected: [MAC_SCREENSHOT] },
|
|
321
|
+
{ name: "a Windows drive path with unescaped spaces", text: WINDOWS_SPACED, expected: [WINDOWS_SPACED] },
|
|
322
|
+
{
|
|
323
|
+
name: "a home-anchored path with unescaped spaces",
|
|
324
|
+
text: "~/Pictures/Cleanshot 2026-07-24 at 12.00.png",
|
|
325
|
+
expected: ["~/Pictures/Cleanshot 2026-07-24 at 12.00.png"],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "a shell-escaped spaced path",
|
|
329
|
+
text: "/tmp/My\\ Photos/shot\\ 1.png",
|
|
330
|
+
expected: ["/tmp/My Photos/shot 1.png"],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "a double-quoted spaced path",
|
|
334
|
+
text: '"/tmp/My Photos/shot 1.png"',
|
|
335
|
+
expected: ["/tmp/My Photos/shot 1.png"],
|
|
336
|
+
},
|
|
337
|
+
{ name: "a spaced path with a non-image extension", text: "/tmp/my report 2026.csv", expected: undefined },
|
|
338
|
+
{
|
|
339
|
+
// Ends in a real image extension, so only the absolute-prefix
|
|
340
|
+
// anchor keeps the whole-text fallback from swallowing the prose.
|
|
341
|
+
name: "prose ending in a path-shaped fragment",
|
|
342
|
+
text: "see /Users/me/Desktop/Screen Shot 1.png",
|
|
343
|
+
expected: undefined,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "two spaced image paths on separate lines",
|
|
347
|
+
text: "/tmp/a shot.png\n/tmp/b shot.png",
|
|
348
|
+
expected: undefined,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "a bare spaced filename with no leading separator",
|
|
352
|
+
text: "Screenshot 2026-07-24 at 1.55.12 PM.png",
|
|
353
|
+
expected: undefined,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "two POSIX paths dragged together when one has unescaped spaces",
|
|
357
|
+
text: "/tmp/a.png /tmp/b shot.png",
|
|
358
|
+
expected: undefined,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "two macOS screenshots dragged together",
|
|
362
|
+
text: `${MAC_SCREENSHOT} /var/folders/xx/T/TemporaryItems/NSIRD_screencaptureui_ab/Screenshot 2026-07-24 at 1.56.00 PM.png`,
|
|
363
|
+
expected: undefined,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: "two home-anchored paths with unescaped spaces",
|
|
367
|
+
text: "~/a.png ~/Pictures/b shot.png",
|
|
368
|
+
expected: undefined,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "two Windows drive paths with unescaped spaces",
|
|
372
|
+
text: `C:\\Users\\me\\a.png ${WINDOWS_SPACED}`,
|
|
373
|
+
expected: undefined,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "two `file://` URLs with unescaped spaces",
|
|
377
|
+
text: "file:///tmp/a.png file:///tmp/b shot.png",
|
|
378
|
+
expected: undefined,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "two UNC paths with unescaped spaces",
|
|
382
|
+
text: "\\\\srv\\share\\a.png \\\\srv\\share\\b shot.png",
|
|
383
|
+
expected: undefined,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "a tab-separated pair of dragged paths",
|
|
387
|
+
text: "/tmp/a.png\t/tmp/b shot.png",
|
|
388
|
+
expected: undefined,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "an absolute path followed by a dot-relative path with unescaped spaces",
|
|
392
|
+
text: "/tmp/a.png ./b shot.png",
|
|
393
|
+
expected: undefined,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "an absolute path followed by a parent-relative path with unescaped spaces",
|
|
397
|
+
text: "/tmp/a.png ../pics/b shot.png",
|
|
398
|
+
expected: undefined,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "a Windows drive path followed by a dot-relative path with unescaped spaces",
|
|
402
|
+
text: "C:\\Users\\me\\a.png .\\b shot.png",
|
|
403
|
+
expected: undefined,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
// The interior `Photos/shot` token after an unescaped space is the
|
|
407
|
+
// shape of a spaced directory name, not of a second dragged path —
|
|
408
|
+
// this is why bare relatives are not multi-path anchors.
|
|
409
|
+
name: "a single path with an unescaped spaced directory name",
|
|
410
|
+
text: "/Users/me/My Photos/shot 1.png",
|
|
411
|
+
expected: ["/Users/me/My Photos/shot 1.png"],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
// The escape asserts the space belongs to the path, so the `/sub`
|
|
415
|
+
// that follows is a component rather than a second drag payload.
|
|
416
|
+
name: "a path whose escaped space precedes a slash-led component",
|
|
417
|
+
text: "/tmp/odd dir\\ /sub/a b.png",
|
|
418
|
+
expected: ["/tmp/odd dir /sub/a b.png"],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
// Splitter-success path: both segments are explicit, so the
|
|
422
|
+
// whole-text pass never runs and the pair still attaches as two.
|
|
423
|
+
name: "two explicit image paths the splitter can separate",
|
|
424
|
+
text: "/tmp/a.png /tmp/b.png",
|
|
425
|
+
expected: ["/tmp/a.png", "/tmp/b.png"],
|
|
426
|
+
},
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
for (const { name, text, expected } of cases) {
|
|
430
|
+
it(`${expected ? "recovers" : "rejects"} ${name} on both paste routes`, () => {
|
|
431
|
+
expect(extractImagePastePathsFromText(text)).toEqual(expected);
|
|
432
|
+
expect(extractBracketedImagePastePaths(bracketedPaste(text))).toEqual(expected);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
267
437
|
describe("CustomEditor space-hold push-to-talk", () => {
|
|
268
438
|
beforeAll(async () => {
|
|
269
439
|
await initTheme();
|
|
@@ -78,14 +78,35 @@ const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
|
78
78
|
const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
79
79
|
const FILE_URI_REGEX = /^file:\/\//i;
|
|
80
80
|
const WINDOWS_DRIVE_PATH_REGEX = /^[a-z]:[\\/]/i;
|
|
81
|
+
/**
|
|
82
|
+
* Alternation of the filesystem prefixes that make a path unambiguously
|
|
83
|
+
* absolute (POSIX root, home, `file://`, UNC, Windows drive). Shared by
|
|
84
|
+
* {@link ABSOLUTE_PATH_PREFIX_REGEX} and {@link INTERIOR_PATH_ANCHOR_REGEX} so
|
|
85
|
+
* the leading-anchor test and the second-anchor test can never disagree about
|
|
86
|
+
* what counts as the start of a path.
|
|
87
|
+
*/
|
|
88
|
+
const ABSOLUTE_PATH_PREFIX_SOURCE = String.raw`(?:\/|~\/|file:\/\/|\\\\|[A-Za-z]:[\\/])`;
|
|
81
89
|
/**
|
|
82
90
|
* Whole-string anchor for paths that are unambiguously absolute. Restricts the
|
|
83
|
-
* "treat the entire
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
91
|
+
* "treat the entire text as one path" pass of {@link extractWholeTextImagePath}
|
|
92
|
+
* to inputs that start with a clearly-anchored filesystem prefix, so prose
|
|
93
|
+
* containing a path-shaped fragment (e.g. "see /tmp/x.png") never hijacks the
|
|
94
|
+
* smart fallback.
|
|
87
95
|
*/
|
|
88
|
-
const ABSOLUTE_PATH_PREFIX_REGEX =
|
|
96
|
+
const ABSOLUTE_PATH_PREFIX_REGEX = new RegExp(`^${ABSOLUTE_PATH_PREFIX_SOURCE}`);
|
|
97
|
+
/**
|
|
98
|
+
* A second path anchor after *unescaped* whitespace — the signature of a
|
|
99
|
+
* multi-path payload (`/tmp/a.png /tmp/b shot.png`, `/tmp/a.png ./b shot.png`)
|
|
100
|
+
* rather than of one path whose name merely contains spaces. Anchors are the
|
|
101
|
+
* absolute prefixes plus dot-relative starts (`./`, `../`, `.\`), which never
|
|
102
|
+
* begin a component of a single sane path. Bare relatives (`dir/b shot.png`)
|
|
103
|
+
* are deliberately NOT anchors: an interior `token/` after a space is exactly
|
|
104
|
+
* the shape of a single path with a spaced directory name
|
|
105
|
+
* (`/Users/me/My Photos/shot 1.png`), which this fallback exists to recover.
|
|
106
|
+
* Escaped whitespace (`/tmp/My\ Photos/x.png`) is exempt: the escape is the
|
|
107
|
+
* terminal asserting the space belongs to the path.
|
|
108
|
+
*/
|
|
109
|
+
const INTERIOR_PATH_ANCHOR_REGEX = new RegExp(String.raw`(?<!\\)\s(?:${ABSOLUTE_PATH_PREFIX_SOURCE}|\.\.?[\\/])`);
|
|
89
110
|
|
|
90
111
|
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
91
112
|
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
@@ -228,16 +249,28 @@ export function extractPastePathsFromText(text: string): string[] | undefined {
|
|
|
228
249
|
return extractExplicitPathSegments(text);
|
|
229
250
|
}
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Whole-text-as-path pass shared by {@link extractImagePastePathsFromText}
|
|
254
|
+
* and {@link extractImagePathFromText}: treat the entire text as one path
|
|
255
|
+
* when it is anchored by {@link ABSOLUTE_PATH_PREFIX_REGEX}, contains no
|
|
256
|
+
* newlines, and points at a supported image extension. Recovers single paths
|
|
257
|
+
* whose unescaped spaces defeat the segment splitter (macOS screenshot names).
|
|
258
|
+
*
|
|
259
|
+
* Refuses payloads carrying a second {@link INTERIOR_PATH_ANCHOR_REGEX} anchor.
|
|
260
|
+
* Dragging two files at once emits `/tmp/a.png /tmp/b shot.png`, which the
|
|
261
|
+
* splitter also refuses (`shot.png` is not explicit); swallowing it as one path
|
|
262
|
+
* attaches nothing, and `handleImagePathPaste`'s ENOENT branch only surfaces a
|
|
263
|
+
* status — unlike its other failure branches it never re-pastes the text — so
|
|
264
|
+
* both paths would vanish. Genuinely ambiguous input lands here too (a
|
|
265
|
+
* directory whose name ends in a space, as in `/tmp/odd dir /sub/x.png`); a
|
|
266
|
+
* plain text paste is the losing-nothing outcome, so ambiguity resolves that way.
|
|
267
|
+
*/
|
|
268
|
+
function extractWholeTextImagePath(text: string): string | undefined {
|
|
269
|
+
const trimmed = text.trim();
|
|
270
|
+
if (!trimmed || /[\r\n]/.test(trimmed) || !ABSOLUTE_PATH_PREFIX_REGEX.test(trimmed)) return undefined;
|
|
271
|
+
if (INTERIOR_PATH_ANCHOR_REGEX.test(trimmed)) return undefined;
|
|
272
|
+
const wholePath = normalizePastedPath(trimmed);
|
|
273
|
+
return wholePath && isExplicitPastedPath(wholePath) && isImagePath(wholePath) ? wholePath : undefined;
|
|
241
274
|
}
|
|
242
275
|
|
|
243
276
|
/**
|
|
@@ -245,10 +278,35 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi
|
|
|
245
278
|
* payload that has already been stripped of the `\x1b[200~` / `\x1b[201~`
|
|
246
279
|
* markers — used by the assembled-paste router in {@link CustomEditor.handleInput}
|
|
247
280
|
* so split bracketed pastes get the same image-path detection as single-chunk ones.
|
|
281
|
+
*
|
|
282
|
+
* When the segment splitter fails (an unescaped space in a real path breaks
|
|
283
|
+
* its every-segment-is-a-path invariant), falls back to
|
|
284
|
+
* {@link extractWholeTextImagePath}, so a dropped macOS screenshot
|
|
285
|
+
* (`Screenshot 2026-06-25 at 1.23.45 PM.png`) attaches as an image instead of
|
|
286
|
+
* degrading to literal text (#6578).
|
|
248
287
|
*/
|
|
249
288
|
export function extractImagePastePathsFromText(text: string): string[] | undefined {
|
|
250
289
|
const paths = extractPastePathsFromText(text);
|
|
251
|
-
return paths
|
|
290
|
+
if (paths !== undefined) return paths.every(isImagePath) ? paths : undefined;
|
|
291
|
+
const wholePath = extractWholeTextImagePath(text);
|
|
292
|
+
return wholePath ? [wholePath] : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function bracketedPastePayload(data: string): string | undefined {
|
|
296
|
+
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
297
|
+
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
298
|
+
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
|
|
299
|
+
return data.slice(BRACKETED_PASTE_START.length, endIndex);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function extractBracketedPastePaths(data: string): string[] | undefined {
|
|
303
|
+
const payload = bracketedPastePayload(data);
|
|
304
|
+
return payload === undefined ? undefined : extractExplicitPathSegments(payload);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function extractBracketedImagePastePaths(data: string): string[] | undefined {
|
|
308
|
+
const payload = bracketedPastePayload(data);
|
|
309
|
+
return payload === undefined ? undefined : extractImagePastePathsFromText(payload);
|
|
252
310
|
}
|
|
253
311
|
|
|
254
312
|
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
@@ -272,25 +330,17 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
|
|
272
330
|
* ambiguous multi-path clipboard text like `/tmp/a.png /tmp/b.png`
|
|
273
331
|
* still falls through to the text fallback instead of being mis-loaded
|
|
274
332
|
* as one giant path).
|
|
275
|
-
* 2.
|
|
276
|
-
* (every segment must look like an explicit path; an unescaped
|
|
277
|
-
* a real path breaks that).
|
|
278
|
-
*
|
|
279
|
-
* fragment ("see /tmp/x.png") never hijacks the smart fallback. This
|
|
280
|
-
* is what recovers macOS screenshot filenames like
|
|
333
|
+
* 2. {@link extractWholeTextImagePath} — only reached when the splitter
|
|
334
|
+
* failed (every segment must look like an explicit path; an unescaped
|
|
335
|
+
* space in a real path breaks that). This is what recovers macOS
|
|
336
|
+
* screenshot filenames like
|
|
281
337
|
* `/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png`.
|
|
282
338
|
*/
|
|
283
339
|
export function extractImagePathFromText(text: string): string | undefined {
|
|
284
340
|
const paths = extractPastePathsFromText(text);
|
|
285
341
|
if (paths?.length === 1 && isImagePath(paths[0])) return paths[0];
|
|
286
342
|
if (paths !== undefined) return undefined;
|
|
287
|
-
|
|
288
|
-
if (!trimmed || /[\r\n]/.test(trimmed) || !ABSOLUTE_PATH_PREFIX_REGEX.test(trimmed)) return undefined;
|
|
289
|
-
const wholePath = normalizePastedPath(trimmed);
|
|
290
|
-
if (wholePath && isExplicitPastedPath(wholePath) && isImagePath(wholePath)) {
|
|
291
|
-
return wholePath;
|
|
292
|
-
}
|
|
293
|
-
return undefined;
|
|
343
|
+
return extractWholeTextImagePath(text);
|
|
294
344
|
}
|
|
295
345
|
|
|
296
346
|
/**
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getPathsForTab,
|
|
19
19
|
getType,
|
|
20
20
|
getUi,
|
|
21
|
+
isCredential,
|
|
21
22
|
SETTING_TABS,
|
|
22
23
|
type SettingPath,
|
|
23
24
|
type SettingTab,
|
|
@@ -192,7 +193,10 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
192
193
|
if (options) {
|
|
193
194
|
return { ...base, type: "submenu", options };
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
+
// One classification drives both surfaces: a setting marked `credential`
|
|
197
|
+
// masks here too, so the panel cannot display one that only the CLI knows
|
|
198
|
+
// to redact.
|
|
199
|
+
return { ...base, type: "text", secret: isCredential(path) };
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
if (schemaType === "array") {
|
|
@@ -2,7 +2,7 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
3
3
|
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
4
4
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { logger, prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
7
7
|
import { extractTextContent } from "../../commit/utils";
|
|
8
8
|
import { settings } from "../../config/settings";
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readArgsHaveTarget,
|
|
16
16
|
} from "../../modes/components/read-tool-group";
|
|
17
17
|
import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
18
|
-
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
18
|
+
import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
|
|
19
19
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
20
20
|
import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
21
21
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
@@ -89,6 +89,17 @@ export class EventController {
|
|
|
89
89
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
90
90
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
91
91
|
#toolTimelineComponents = new Map<string, Component>();
|
|
92
|
+
// Completions that arrived before any component existed for their call id.
|
|
93
|
+
// Cursor's server-resolved tools (todo) emit `tool_execution_end` through a
|
|
94
|
+
// synchronous callback fired mid-parse, while the `toolcall_start` for the
|
|
95
|
+
// same call rides `AssistantMessageEventStream` and is delivered a microtask
|
|
96
|
+
// later. When the server packs start and completion into one HTTP/2 chunk
|
|
97
|
+
// the completion is handled FIRST — with no `pendingTools` entry to settle.
|
|
98
|
+
// Dropping it would strand the card the streamed block creates moments
|
|
99
|
+
// later, so the event is held here and replayed the moment that component
|
|
100
|
+
// materializes (`#handleMessageUpdate`). Keyed by call id; ids are unique
|
|
101
|
+
// per turn, and the map is cleared with the other transcript anchors.
|
|
102
|
+
#orphanedToolCompletions = new Map<string, Extract<AgentSessionEvent, { type: "tool_execution_end" }>>();
|
|
92
103
|
#postToolAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
93
104
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
94
105
|
// Assistant component whose turn-ending error is currently mirrored in the
|
|
@@ -334,6 +345,7 @@ export class EventController {
|
|
|
334
345
|
this.#renderedCustomMessages.clear();
|
|
335
346
|
this.#lastIntent = undefined;
|
|
336
347
|
this.#toolTimelineComponents.clear();
|
|
348
|
+
this.#orphanedToolCompletions.clear();
|
|
337
349
|
this.#postToolAssistantComponents.clear();
|
|
338
350
|
this.#backgroundTaskCallIds.clear();
|
|
339
351
|
this.#approvalAttentionToolCallIds.clear();
|
|
@@ -422,6 +434,7 @@ export class EventController {
|
|
|
422
434
|
|
|
423
435
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
424
436
|
this.#toolTimelineComponents.clear();
|
|
437
|
+
this.#orphanedToolCompletions.clear();
|
|
425
438
|
this.#postToolAssistantComponents.clear();
|
|
426
439
|
this.#lastIntent = undefined;
|
|
427
440
|
this.#readToolCallArgs.clear();
|
|
@@ -777,7 +790,14 @@ export class EventController {
|
|
|
777
790
|
this.#toolArgsReveal.finish(content.id);
|
|
778
791
|
renderArgs = content.arguments;
|
|
779
792
|
}
|
|
780
|
-
|
|
793
|
+
// `message_update` is cumulative — every update re-lists all blocks
|
|
794
|
+
// of the streaming message — so creation must also be guarded by the
|
|
795
|
+
// timeline map: `pendingTools` loses the id the moment a completion
|
|
796
|
+
// settles the card, and for server-resolved (Cursor) tools that can
|
|
797
|
+
// happen while the message is still streaming. Without the second
|
|
798
|
+
// check the next cumulative update would recreate a card for a call
|
|
799
|
+
// that already finished, permanently pending.
|
|
800
|
+
if (!this.ctx.pendingTools.has(content.id) && !this.#toolTimelineComponents.has(content.id)) {
|
|
781
801
|
this.#resolveDisplaceablePoll(content.name);
|
|
782
802
|
this.#resetReadGroup();
|
|
783
803
|
const component = new ToolExecutionComponent(
|
|
@@ -799,6 +819,16 @@ export class EventController {
|
|
|
799
819
|
this.ctx.pendingTools.set(content.id, component);
|
|
800
820
|
this.#toolTimelineComponents.set(content.id, component);
|
|
801
821
|
this.#toolArgsReveal.bind(content.id, component);
|
|
822
|
+
// A held completion for this call means its `tool_execution_end`
|
|
823
|
+
// outran this streamed block (see #orphanedToolCompletions).
|
|
824
|
+
// Attach it now that the card exists so it settles immediately
|
|
825
|
+
// instead of animating forever. Only the component is settled —
|
|
826
|
+
// the handler's other side effects already ran on first arrival.
|
|
827
|
+
const orphan = this.#orphanedToolCompletions.get(content.id);
|
|
828
|
+
if (orphan) {
|
|
829
|
+
this.#orphanedToolCompletions.delete(content.id);
|
|
830
|
+
this.#settleHeldCompletion(component, orphan);
|
|
831
|
+
}
|
|
802
832
|
} else {
|
|
803
833
|
const component = this.ctx.pendingTools.get(content.id);
|
|
804
834
|
if (component) {
|
|
@@ -1066,6 +1096,44 @@ export class EventController {
|
|
|
1066
1096
|
}
|
|
1067
1097
|
}
|
|
1068
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Attach a held completion to the component that was created for it after
|
|
1101
|
+
* the fact (see {@link #orphanedToolCompletions}) and settle the card.
|
|
1102
|
+
*
|
|
1103
|
+
* Deliberately NOT a re-entry into `#handleToolExecutionEnd`: every
|
|
1104
|
+
* user-facing side effect of that handler — the todo panel refresh, the
|
|
1105
|
+
* failure warning, plan approval, the terminal-title transition — already
|
|
1106
|
+
* ran when the completion first arrived. Replaying the whole handler would
|
|
1107
|
+
* show the same warning twice and re-push an identical `setTodos`. Only the
|
|
1108
|
+
* component half was missed, because the component did not exist yet.
|
|
1109
|
+
*/
|
|
1110
|
+
#settleHeldCompletion(
|
|
1111
|
+
component: ToolExecutionHandle,
|
|
1112
|
+
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
1113
|
+
): void {
|
|
1114
|
+
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
1115
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
1116
|
+
if (
|
|
1117
|
+
component instanceof ToolExecutionComponent &&
|
|
1118
|
+
component.isDisplaceableBlock() &&
|
|
1119
|
+
event.toolName === "todo" &&
|
|
1120
|
+
component.canBeDisplacedBy("todo")
|
|
1121
|
+
) {
|
|
1122
|
+
// Mirrors the displacement bookkeeping in `#handleToolExecutionEnd`:
|
|
1123
|
+
// a successful snapshot supersedes the previous live panel.
|
|
1124
|
+
const previous = this.#displaceableTodoComponent;
|
|
1125
|
+
if (previous && previous !== component && previous.isDisplaceableBlock()) {
|
|
1126
|
+
this.#displaceableTodoComponent = undefined;
|
|
1127
|
+
if (this.ctx.chatContainer.isBlockUncommitted(previous)) {
|
|
1128
|
+
this.ctx.chatContainer.removeChild(previous);
|
|
1129
|
+
}
|
|
1130
|
+
previous.seal();
|
|
1131
|
+
}
|
|
1132
|
+
this.#displaceableTodoComponent = component;
|
|
1133
|
+
}
|
|
1134
|
+
this.ctx.ui.requestRender();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1069
1137
|
async #handleToolExecutionEnd(event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>): Promise<void> {
|
|
1070
1138
|
// A transient overlay (auto-compaction / auto-retry / handoff) that ran
|
|
1071
1139
|
// between this tool's start and end could have detached the working
|
|
@@ -1142,6 +1210,15 @@ export class EventController {
|
|
|
1142
1210
|
}
|
|
1143
1211
|
}
|
|
1144
1212
|
this.ctx.ui.requestRender();
|
|
1213
|
+
} else if (event.toolName === "todo") {
|
|
1214
|
+
// No component yet: the streamed block that creates the card has not
|
|
1215
|
+
// been delivered (see #orphanedToolCompletions). Hold the completion
|
|
1216
|
+
// for replay instead of dropping it — scoped to `todo`, the only
|
|
1217
|
+
// tool whose completion is emitted synchronously mid-parse. The
|
|
1218
|
+
// panel/warning side effects below still run NOW, on first arrival;
|
|
1219
|
+
// the replay settles only the component
|
|
1220
|
+
// (`#settleHeldCompletion`), so neither is repeated.
|
|
1221
|
+
this.#orphanedToolCompletions.set(event.toolCallId, event);
|
|
1145
1222
|
}
|
|
1146
1223
|
}
|
|
1147
1224
|
// Update todo display when todo tool completes
|
|
@@ -1154,8 +1231,22 @@ export class EventController {
|
|
|
1154
1231
|
const textContent = event.result.content.find(
|
|
1155
1232
|
(content: { type: string; text?: string }) => content.type === "text",
|
|
1156
1233
|
)?.text;
|
|
1234
|
+
// This text can be a provider error copied verbatim off the wire (the
|
|
1235
|
+
// Cursor todo bridge forwards the server's string), so it may carry
|
|
1236
|
+
// ANSI escapes, other C0/C1 controls, tabs, newlines, or a line far
|
|
1237
|
+
// wider than the terminal. `showWarning` renders through a plain
|
|
1238
|
+
// `Text`, which strips none of that — an escape reaches the terminal
|
|
1239
|
+
// and can repaint outside the row. `sanitizeText` drops the control
|
|
1240
|
+
// sequences (and returns the same reference when there are none),
|
|
1241
|
+
// then `previewLine` collapses the remaining whitespace and bounds
|
|
1242
|
+
// the width. Sanitizing first matters: truncating before stripping
|
|
1243
|
+
// can cut an escape mid-sequence and leave a dangling introducer.
|
|
1244
|
+
//
|
|
1245
|
+
// This is the render boundary, not the persisted result: the stored
|
|
1246
|
+
// error stays full-fidelity for the transcript and for replays.
|
|
1247
|
+
const detail = textContent ? previewLine(sanitizeText(textContent), TRUNCATE_LENGTHS.LINE) : "";
|
|
1157
1248
|
this.ctx.showWarning(
|
|
1158
|
-
`Todo update failed${
|
|
1249
|
+
`Todo update failed${detail ? `: ${detail}` : ". Progress may be stale until todo succeeds."}`,
|
|
1159
1250
|
);
|
|
1160
1251
|
}
|
|
1161
1252
|
// Plan approval rides a `write` to xd://propose: the dispatch metadata on
|
|
@@ -1236,6 +1327,7 @@ export class EventController {
|
|
|
1236
1327
|
this.#readToolCallArgs.clear();
|
|
1237
1328
|
this.#readToolCallAssistantComponents.clear();
|
|
1238
1329
|
this.#toolTimelineComponents.clear();
|
|
1330
|
+
this.#orphanedToolCompletions.clear();
|
|
1239
1331
|
this.#postToolAssistantComponents.clear();
|
|
1240
1332
|
this.#resetReadGroup();
|
|
1241
1333
|
// The turn is over: nothing else lands this turn, so the waiting poll is
|
|
@@ -420,6 +420,11 @@ export class SelectorController {
|
|
|
420
420
|
this.ctx.session.setAutoCompactionEnabled(value as boolean);
|
|
421
421
|
this.ctx.statusLine.setAutoCompactEnabled(value as boolean);
|
|
422
422
|
break;
|
|
423
|
+
case "advisor.enabled":
|
|
424
|
+
this.ctx.session.setAdvisorEnabled(value as boolean);
|
|
425
|
+
this.ctx.statusLine.invalidate();
|
|
426
|
+
this.ctx.ui.requestRender();
|
|
427
|
+
break;
|
|
423
428
|
case "steeringMode":
|
|
424
429
|
this.ctx.session.setSteeringMode(value as "all" | "one-at-a-time");
|
|
425
430
|
break;
|
|
@@ -1270,6 +1275,15 @@ export class SelectorController {
|
|
|
1270
1275
|
this.ctx.editor.setText(result.editorText);
|
|
1271
1276
|
}
|
|
1272
1277
|
this.ctx.showStatus("Navigated to selected point");
|
|
1278
|
+
|
|
1279
|
+
// Re-answering a past `ask` commits a new sibling answer but,
|
|
1280
|
+
// unlike a live `ask`, leaves the agent idle. Resume it now —
|
|
1281
|
+
// after the transcript rebuild above — so the model consumes the
|
|
1282
|
+
// new answer without the resumed turn rendering against the stale
|
|
1283
|
+
// pre-rebuild UI (issue #6483).
|
|
1284
|
+
if (result.askReanswerCommitted) {
|
|
1285
|
+
this.ctx.session.resumeAfterAskReanswer();
|
|
1286
|
+
}
|
|
1273
1287
|
} catch (error) {
|
|
1274
1288
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1275
1289
|
} finally {
|
|
@@ -1395,6 +1395,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1395
1395
|
return;
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
|
+
if (action === "reset" && this.vibeModeEnabled) {
|
|
1399
|
+
this.disableLoopMode("Exit vibe mode before using reset loops. Loop mode disabled.");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1398
1403
|
if (!consumeLoopLimitIteration(this.loopLimit)) {
|
|
1399
1404
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
1400
1405
|
return;
|
|
@@ -3708,6 +3713,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3708
3713
|
return;
|
|
3709
3714
|
}
|
|
3710
3715
|
|
|
3716
|
+
// resolveApprovedPlan may return a newer draft than the path recorded in
|
|
3717
|
+
// plan-mode state. `AgentSession.#buildPlanModeMessage()` reads that state,
|
|
3718
|
+
// so if the operator refines (or dismisses and keeps planning) the next
|
|
3719
|
+
// planning turn must target the plan just reviewed — promote the reviewed
|
|
3720
|
+
// path into plan-mode state now, mirroring the print-mode approval handler.
|
|
3721
|
+
const planState = this.session.getPlanModeState();
|
|
3722
|
+
if (planState?.enabled && planState.planFilePath !== planFilePath) {
|
|
3723
|
+
this.session.setPlanModeState({ ...planState, planFilePath });
|
|
3724
|
+
this.sessionManager.appendModeChange("plan", { planFilePath });
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3711
3727
|
const contextUsage = this.#getPlanApprovalContextUsage();
|
|
3712
3728
|
const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
|
|
3713
3729
|
const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
|
|
@@ -4432,6 +4448,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4432
4448
|
this.#commandController.handleContextCommand();
|
|
4433
4449
|
}
|
|
4434
4450
|
|
|
4451
|
+
#vibeSessionTransitionBlocked(): boolean {
|
|
4452
|
+
if (!this.vibeModeEnabled) return false;
|
|
4453
|
+
this.showWarning("Exit vibe mode first.");
|
|
4454
|
+
return true;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4435
4457
|
#prepareSessionSwitch(): void {
|
|
4436
4458
|
this.#btwController.dispose();
|
|
4437
4459
|
this.#omfgController.dispose();
|
|
@@ -4440,28 +4462,32 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4440
4462
|
this.#hidePlanReview();
|
|
4441
4463
|
}
|
|
4442
4464
|
|
|
4443
|
-
handleClearCommand(): Promise<void> {
|
|
4465
|
+
async handleClearCommand(): Promise<void> {
|
|
4466
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4444
4467
|
this.#prepareSessionSwitch();
|
|
4445
|
-
|
|
4468
|
+
await this.#commandController.handleClearCommand();
|
|
4446
4469
|
}
|
|
4447
4470
|
|
|
4448
4471
|
handleFreshCommand(): Promise<void> {
|
|
4449
4472
|
return this.#commandController.handleFreshCommand();
|
|
4450
4473
|
}
|
|
4451
4474
|
|
|
4452
|
-
handleDropCommand(): Promise<void> {
|
|
4475
|
+
async handleDropCommand(): Promise<void> {
|
|
4476
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4453
4477
|
this.#prepareSessionSwitch();
|
|
4454
|
-
|
|
4478
|
+
await this.#commandController.handleDropCommand();
|
|
4455
4479
|
}
|
|
4456
4480
|
|
|
4457
|
-
handleForkCommand(): Promise<void> {
|
|
4481
|
+
async handleForkCommand(): Promise<void> {
|
|
4482
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4458
4483
|
this.#btwController.dispose();
|
|
4459
4484
|
this.#omfgController.dispose();
|
|
4460
|
-
|
|
4485
|
+
await this.#commandController.handleForkCommand();
|
|
4461
4486
|
}
|
|
4462
4487
|
|
|
4463
|
-
handleMoveCommand(targetPath?: string): Promise<void> {
|
|
4464
|
-
|
|
4488
|
+
async handleMoveCommand(targetPath?: string): Promise<void> {
|
|
4489
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4490
|
+
await this.#commandController.handleMoveCommand(targetPath);
|
|
4465
4491
|
}
|
|
4466
4492
|
|
|
4467
4493
|
handleRenameCommand(title: string): Promise<void> {
|