bermudis-pi-goodies 0.27.1 → 0.27.3
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/README.md +8 -6
- package/goodies.ts +0 -2
- package/index.ts +0 -3
- package/package.json +1 -1
- package/review.ts +1 -1
- package/side.ts +238 -29
- package/zed.ts +0 -38
package/README.md
CHANGED
|
@@ -8,7 +8,6 @@ extensions. One entry point, thirteen independent features.
|
|
|
8
8
|
| `copy-with-model` | `/copy-with-model` | Copy last assistant message to the clipboard in a code fence tagged with the model name. |
|
|
9
9
|
| `copy-trajectory` | `/copy-trajectory [thinking]` | Copy the whole conversation (user + assistant text, tool calls stripped) to the clipboard; `thinking` also includes assistant thinking blocks. |
|
|
10
10
|
| `name-with-ai` | `/name-with-ai [name]` | Generate a short session name from the first user message (or set one manually). |
|
|
11
|
-
| `zed` | `/z` | Open Zed editor on the current working directory. |
|
|
12
11
|
| `prefer-tools` | hook (no command) | Blocks legacy tools: `rm` → `trash`, bare `python`/`pip`/`pytest`/`mypy` → `uv`. On `glm-5.*` models it also blocks bash `rg` (they invent ripgrep flags) — the built-in grep tool still works. |
|
|
13
12
|
| `keep-model-on-new` | hook (no command) | Keep the active model when `/new` starts a fresh session instead of reverting to pi's saved default model. |
|
|
14
13
|
| `model-thinking` | `/model-thinking` | Per-model default thinking levels: save the current level as this model's default and get it back on every switch to that model, instead of pi's global default. |
|
|
@@ -26,7 +25,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
26
25
|
First install, pinned to a published version:
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
pi install npm:bermudis-pi-goodies@0.27.
|
|
28
|
+
pi install npm:bermudis-pi-goodies@0.27.3
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
Picking up a new release on an existing install (`pi install` is not the
|
|
@@ -438,10 +437,13 @@ Details worth knowing:
|
|
|
438
437
|
path. (`deliverAs: "nextTurn"` would only queue in memory and die with
|
|
439
438
|
the process.)
|
|
440
439
|
- The footer's context-usage estimate reflects the raw session branch, not
|
|
441
|
-
the lens output, so it over-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
440
|
+
the lens output, so it can over-report while a side session is active.
|
|
441
|
+
Threshold auto-compaction is vetoed when the **lensed side request**
|
|
442
|
+
fits the side model's window (including system/tool overhead and the
|
|
443
|
+
configured token reserve), rather than trusting the main model's usage.
|
|
444
|
+
The quoted main transcript still counts toward that window. Manual
|
|
445
|
+
`/compact` and actual context-overflow recovery remain available.
|
|
446
|
+
Compaction summaries that cover side
|
|
445
447
|
turns are quoted with an explicit who-is-who caveat instead of passing as
|
|
446
448
|
the consultant's own history.
|
|
447
449
|
- Both quote directions carry an explicit untrusted-evidence preamble:
|
package/goodies.ts
CHANGED
|
@@ -34,7 +34,6 @@ type FeatureName =
|
|
|
34
34
|
| "copy-with-model"
|
|
35
35
|
| "copy-trajectory"
|
|
36
36
|
| "name-with-ai"
|
|
37
|
-
| "zed"
|
|
38
37
|
| "prefer-tools"
|
|
39
38
|
| "keep-model"
|
|
40
39
|
| "model-thinking"
|
|
@@ -50,7 +49,6 @@ const FEATURES: FeatureName[] = [
|
|
|
50
49
|
"copy-with-model",
|
|
51
50
|
"copy-trajectory",
|
|
52
51
|
"name-with-ai",
|
|
53
|
-
"zed",
|
|
54
52
|
"prefer-tools",
|
|
55
53
|
"keep-model",
|
|
56
54
|
"model-thinking",
|
package/index.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* - copy-with-model /copy-with-model copy last reply tagged with the model
|
|
7
7
|
* - copy-trajectory /copy-trajectory copy the whole conversation (text only) to the clipboard
|
|
8
8
|
* - name-with-ai /name-with-ai generate a session name via the model
|
|
9
|
-
* - zed /z open Zed on cwd
|
|
10
9
|
* - prefer-tools hook block legacy tools (trash/uv; blocks rg on glm-5.*)
|
|
11
10
|
* - keep-model hook preserve the active model across /new
|
|
12
11
|
* - model-thinking /model-thinking per-model default thinking levels, applied on switch
|
|
@@ -38,7 +37,6 @@ import tps from "./tps.ts";
|
|
|
38
37
|
import vision from "./vision.ts";
|
|
39
38
|
import side from "./side.ts";
|
|
40
39
|
import nameWithAi from "./name-with-ai.ts";
|
|
41
|
-
import zed from "./zed.ts";
|
|
42
40
|
import preferTools from "./prefer-tools.ts";
|
|
43
41
|
import goodies, { isEnabled } from "./goodies.ts";
|
|
44
42
|
import { setCleanTuiActive } from "./clean-tui.ts";
|
|
@@ -51,7 +49,6 @@ export default function bermudisPiGoodies(pi: ExtensionAPI): void {
|
|
|
51
49
|
if (isEnabled("copy-with-model")) copyWithModel(pi);
|
|
52
50
|
if (isEnabled("copy-trajectory")) copyTrajectory(pi);
|
|
53
51
|
if (isEnabled("name-with-ai")) nameWithAi(pi);
|
|
54
|
-
if (isEnabled("zed")) zed(pi);
|
|
55
52
|
if (isEnabled("prefer-tools")) preferTools(pi);
|
|
56
53
|
if (isEnabled("keep-model")) keepModelOnNew(pi);
|
|
57
54
|
if (isEnabled("model-thinking")) modelThinking(pi);
|
package/package.json
CHANGED
package/review.ts
CHANGED
|
@@ -2125,9 +2125,9 @@ Instructions:
|
|
|
2125
2125
|
endReviewInProgress = true;
|
|
2126
2126
|
try {
|
|
2127
2127
|
const choice = await ctx.ui.select("Finish review:", [
|
|
2128
|
-
"Return only",
|
|
2129
2128
|
"Return and fix findings",
|
|
2130
2129
|
"Return and summarize",
|
|
2130
|
+
"Return only",
|
|
2131
2131
|
]);
|
|
2132
2132
|
|
|
2133
2133
|
if (choice === undefined) {
|
package/side.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* - /side-exit navigates back to the recorded tip, restores the main model,
|
|
16
16
|
* and hands the main agent the side conversation — also as an attributed
|
|
17
17
|
* quote — with the mode chosen at exit: trajectory (full transcript),
|
|
18
|
-
* summary (the side model summarizes
|
|
18
|
+
* summary (the side model summarizes, shown as a cancellable progress
|
|
19
|
+
* widget above the editor), or nothing.
|
|
19
20
|
*
|
|
20
21
|
* Durability: handoffs are appended as custom-message entries via
|
|
21
22
|
* sendMessage({ triggerTurn: false }) while idle, which writes the session
|
|
@@ -28,11 +29,10 @@
|
|
|
28
29
|
* limb that is ever resumed delivers only newer turns. v1 always starts a
|
|
29
30
|
* fresh side limb, but the bookkeeping keeps that future-safe.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* is compaction-aware and survives it; the main limb is never touched).
|
|
32
|
+
* Pi's footer context-usage estimate reflects the raw session branch, not
|
|
33
|
+
* the lens output, so the footer over-reports during side sessions. A
|
|
34
|
+
* session_before_compact guard vetoes threshold compaction when the lensed
|
|
35
|
+
* request fits; manual compaction and actual overflow recovery still run.
|
|
36
36
|
* The side badge carries pi's own estimator applied to the lensed messages
|
|
37
37
|
* ("· ~46k lensed") as the honest counterpoint to the inflated figure.
|
|
38
38
|
*/
|
|
@@ -44,11 +44,14 @@ import type {
|
|
|
44
44
|
ExtensionCommandContext,
|
|
45
45
|
ExtensionContext,
|
|
46
46
|
SessionEntry,
|
|
47
|
+
ToolInfo,
|
|
47
48
|
} from "@earendil-works/pi-coding-agent";
|
|
48
49
|
import {
|
|
49
50
|
ModelSelectorComponent,
|
|
51
|
+
calculateContextTokens,
|
|
50
52
|
estimateTokens,
|
|
51
53
|
sessionEntryToContextMessages,
|
|
54
|
+
shouldCompact,
|
|
52
55
|
} from "@earendil-works/pi-coding-agent";
|
|
53
56
|
import { describeError, extractTextParts } from "./json-file.ts";
|
|
54
57
|
import { buildTrajectory, renderTrajectory } from "./copy-trajectory.ts";
|
|
@@ -321,6 +324,50 @@ export function estimateLensedContextTokens(
|
|
|
321
324
|
return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
322
325
|
}
|
|
323
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Conservative side-request budget. Main-model usage cannot leak into this
|
|
329
|
+
* estimate: its assistant turns have already become quoted user text.
|
|
330
|
+
* Include system/tool overhead before the first side response, and trust
|
|
331
|
+
* valid SIDE usage as a floor (it includes provider-visible overhead).
|
|
332
|
+
*/
|
|
333
|
+
export function estimateSideCompactionTokens(
|
|
334
|
+
messages: ContextEvent["messages"],
|
|
335
|
+
systemPrompt: string,
|
|
336
|
+
tools: readonly Pick<ToolInfo, "name" | "description" | "parameters">[],
|
|
337
|
+
rawBranch: readonly SessionEntry[],
|
|
338
|
+
): number {
|
|
339
|
+
// Kept responses still carry pre-compaction usage. Match by source
|
|
340
|
+
// message identity, not timestamp: branch order is the provenance Pi uses.
|
|
341
|
+
const lastInvalidation = rawBranch.findLastIndex(
|
|
342
|
+
(entry) => entry.type === "compaction" || entry.type === "context_edit",
|
|
343
|
+
);
|
|
344
|
+
const validUsageMessages = new Set(
|
|
345
|
+
rawBranch
|
|
346
|
+
.slice(lastInvalidation + 1)
|
|
347
|
+
.flatMap((entry) => sessionEntryToContextMessages(entry)),
|
|
348
|
+
);
|
|
349
|
+
const estimated =
|
|
350
|
+
messages.reduce((sum, message) => sum + estimateTokens(message), 0) +
|
|
351
|
+
Math.ceil(systemPrompt.length / 4) +
|
|
352
|
+
Math.ceil(JSON.stringify(tools).length / 4);
|
|
353
|
+
let trailing = 0;
|
|
354
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
355
|
+
const message = messages[i]!;
|
|
356
|
+
if (
|
|
357
|
+
message.role === "assistant" &&
|
|
358
|
+
validUsageMessages.has(message) &&
|
|
359
|
+
message.stopReason !== "error" &&
|
|
360
|
+
message.stopReason !== "aborted" &&
|
|
361
|
+
message.usage
|
|
362
|
+
) {
|
|
363
|
+
const usage = calculateContextTokens(message.usage);
|
|
364
|
+
if (usage > 0) return Math.max(estimated, usage + trailing);
|
|
365
|
+
}
|
|
366
|
+
trailing += estimateTokens(message);
|
|
367
|
+
}
|
|
368
|
+
return estimated;
|
|
369
|
+
}
|
|
370
|
+
|
|
324
371
|
// ---------------------------------------------------------------------------
|
|
325
372
|
// Handoff: what the main agent is given at exit
|
|
326
373
|
// ---------------------------------------------------------------------------
|
|
@@ -590,10 +637,24 @@ function refreshSideLens(ctx: ExtensionContext): void {
|
|
|
590
637
|
setSideLensTokens(tokens);
|
|
591
638
|
}
|
|
592
639
|
|
|
640
|
+
/**
|
|
641
|
+
* The summarizer's completion resolved with stopReason "aborted" (partial
|
|
642
|
+
* text after an Esc) instead of rejecting — the caller's abort-signal check
|
|
643
|
+
* never fires on that path, so this sentinel carries the cancel out to the
|
|
644
|
+
* SummaryOutcome boundary below.
|
|
645
|
+
*/
|
|
646
|
+
class SummaryAbortedError extends Error {
|
|
647
|
+
constructor() {
|
|
648
|
+
super("side model summary was aborted");
|
|
649
|
+
this.name = "SummaryAbortedError";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
593
653
|
async function summarizeDelta(
|
|
594
654
|
ctx: ExtensionCommandContext,
|
|
595
655
|
sideModel: ModelRef,
|
|
596
656
|
transcript: string,
|
|
657
|
+
signal?: AbortSignal,
|
|
597
658
|
): Promise<string> {
|
|
598
659
|
const model = ctx.modelRegistry.find(sideModel.provider, sideModel.id);
|
|
599
660
|
if (!model) {
|
|
@@ -601,22 +662,116 @@ async function summarizeDelta(
|
|
|
601
662
|
`${modelRef(sideModel)} is no longer available to summarize — use trajectory or nothing`,
|
|
602
663
|
);
|
|
603
664
|
}
|
|
604
|
-
const response = await ctx.modelRegistry.complete(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
665
|
+
const response = await ctx.modelRegistry.complete(
|
|
666
|
+
model,
|
|
667
|
+
{
|
|
668
|
+
messages: [
|
|
669
|
+
{
|
|
670
|
+
role: "user",
|
|
671
|
+
timestamp: Date.now(),
|
|
672
|
+
content: [
|
|
673
|
+
{ type: "text", text: `${SUMMARY_INSTRUCTIONS}\n\n${transcript}` },
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
{ signal },
|
|
679
|
+
);
|
|
615
680
|
const text = extractTextParts(response.content).join("\n").trim();
|
|
681
|
+
// complete() RESOLVES failed and aborted calls (providers mark stopReason
|
|
682
|
+
// "error" or "aborted" and keep the partial text) rather than rejecting.
|
|
683
|
+
// Left alone, a truncated reply would reach the main agent as a finished
|
|
684
|
+
// summary. "aborted" becomes the cancel sentinel; "error" becomes a real
|
|
685
|
+
// failure so it rides the fail-closed exit path with the provider's message.
|
|
686
|
+
if (response.stopReason === "aborted") throw new SummaryAbortedError();
|
|
687
|
+
if (response.stopReason === "error") {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`side model summary failed: ${response.errorMessage ?? "unknown error"}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
616
692
|
if (!text) throw new Error("side model returned an empty summary");
|
|
617
693
|
return text;
|
|
618
694
|
}
|
|
619
695
|
|
|
696
|
+
type SummaryOutcome =
|
|
697
|
+
| { ok: true; text: string }
|
|
698
|
+
| { ok: false; cancelled: true }
|
|
699
|
+
| { ok: false; cancelled: false; error: unknown };
|
|
700
|
+
|
|
701
|
+
const SUMMARIZE_WIDGET_KEY = "bermudis-pi-goodies.side-summarize";
|
|
702
|
+
|
|
703
|
+
/** Fallback budget when there is no TTY to measure (tests, piped output). */
|
|
704
|
+
const WIDGET_LINE_FALLBACK_WIDTH = 80;
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Visible budget for the summarize progress line. Pi renders string-array
|
|
708
|
+
* widgets as Text(line, 1, 0) — one column of side padding — so the line
|
|
709
|
+
* wraps past (terminal width − 2). Without a TTY fall back to the
|
|
710
|
+
* historical 80 and never grow past it, so known-width terminals only ever
|
|
711
|
+
* truncate harder (same pattern as clean-tui's bashLineCap).
|
|
712
|
+
*/
|
|
713
|
+
function widgetLineCap(): number {
|
|
714
|
+
const cols = process.stdout.columns;
|
|
715
|
+
if (typeof cols !== "number" || cols <= 0)
|
|
716
|
+
return WIDGET_LINE_FALLBACK_WIDTH;
|
|
717
|
+
return Math.max(20, Math.min(WIDGET_LINE_FALLBACK_WIDTH, cols - 2));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Widget lines render above the editor; stay single-line on narrow terminals. */
|
|
721
|
+
function truncateWidgetLine(text: string, max = widgetLineCap()): string {
|
|
722
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Exit summarization with visible, cancellable UI: a progress widget above
|
|
727
|
+
* the editor for the whole call, Esc to abort. The picker closes the moment
|
|
728
|
+
* the mode is chosen; without these the UI then sits silent until the model
|
|
729
|
+
* answers, which reads as a freeze on long consultations.
|
|
730
|
+
*
|
|
731
|
+
* setWidget/onTerminalInput are optional-called because harness stubs and
|
|
732
|
+
* limited (non-interactive) contexts lack them; headless keeps the old
|
|
733
|
+
* no-feedback behavior, which is all it ever had.
|
|
734
|
+
*/
|
|
735
|
+
export async function summarizeWithProgress(
|
|
736
|
+
ctx: ExtensionCommandContext,
|
|
737
|
+
sideModel: ModelRef,
|
|
738
|
+
transcript: string,
|
|
739
|
+
turnCount: number,
|
|
740
|
+
): Promise<SummaryOutcome> {
|
|
741
|
+
const abort = new AbortController();
|
|
742
|
+
const turns = `${turnCount} ${turnCount === 1 ? "turn" : "turns"}`;
|
|
743
|
+
const line = truncateWidgetLine(
|
|
744
|
+
`⟳ summarizing side consultation (${turns}) with ${modelRef(sideModel)} — esc cancels`,
|
|
745
|
+
);
|
|
746
|
+
const unsubscribe = ctx.ui?.onTerminalInput?.((data) => {
|
|
747
|
+
// A bare Esc arrives as exactly "\x1b" (pi's stdin buffer flushes a lone
|
|
748
|
+
// ESC after its escape timeout); sequences like arrow keys arrive as
|
|
749
|
+
// longer chunks and pass through untouched.
|
|
750
|
+
if (data === "\x1b") {
|
|
751
|
+
abort.abort();
|
|
752
|
+
return { consume: true };
|
|
753
|
+
}
|
|
754
|
+
return undefined;
|
|
755
|
+
});
|
|
756
|
+
ctx.ui?.setWidget?.(SUMMARIZE_WIDGET_KEY, [line]);
|
|
757
|
+
try {
|
|
758
|
+
const text = await summarizeDelta(ctx, sideModel, transcript, abort.signal);
|
|
759
|
+
return { ok: true, text };
|
|
760
|
+
} catch (error) {
|
|
761
|
+
// Cancellation reaches here two ways: our own Esc (signal aborted, which
|
|
762
|
+
// may surface as a rejection or a resolved "aborted" message that
|
|
763
|
+
// summarizeDelta turns into the sentinel). Either way the user asked to
|
|
764
|
+
// stop, so it rides the stay-in-side-session path. Every other error,
|
|
765
|
+
// including a resolved "error" message, rides the fail-closed exit path.
|
|
766
|
+
if (abort.signal.aborted || error instanceof SummaryAbortedError)
|
|
767
|
+
return { ok: false, cancelled: true };
|
|
768
|
+
return { ok: false, cancelled: false, error };
|
|
769
|
+
} finally {
|
|
770
|
+
unsubscribe?.();
|
|
771
|
+
ctx.ui?.setWidget?.(SUMMARIZE_WIDGET_KEY, undefined);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
620
775
|
// ---------------------------------------------------------------------------
|
|
621
776
|
// Extension entry point
|
|
622
777
|
// ---------------------------------------------------------------------------
|
|
@@ -627,6 +782,52 @@ let modelRegistryRef: ExtensionContext["modelRegistry"] | undefined;
|
|
|
627
782
|
let lastBadgeCtx: ExtensionContext | undefined;
|
|
628
783
|
|
|
629
784
|
export default function side(pi: ExtensionAPI): void {
|
|
785
|
+
// Pi checks the raw projection BEFORE emitting `context`. Recheck the
|
|
786
|
+
// same threshold against our lens at its supported cancellation boundary.
|
|
787
|
+
// Never veto manual compaction or a real provider overflow.
|
|
788
|
+
pi.on("session_before_compact", (event, ctx) => {
|
|
789
|
+
if (event.reason !== "threshold" || !ctx.model) return;
|
|
790
|
+
const raw = ctx.sessionManager.getBranch();
|
|
791
|
+
const markerIdx = findSideBoundary(raw);
|
|
792
|
+
const marker = raw[markerIdx];
|
|
793
|
+
if (marker?.type !== "custom" || !parseSideMarkerData(marker.data)) return;
|
|
794
|
+
const messages = buildLensMessages(
|
|
795
|
+
ctx.sessionManager.buildContextEntries(),
|
|
796
|
+
raw,
|
|
797
|
+
markerIdx,
|
|
798
|
+
[],
|
|
799
|
+
);
|
|
800
|
+
if (!messages || ctx.model.contextWindow <= 0) return;
|
|
801
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
802
|
+
const tokens = estimateSideCompactionTokens(
|
|
803
|
+
messages,
|
|
804
|
+
ctx.getSystemPrompt(),
|
|
805
|
+
pi
|
|
806
|
+
.getAllTools()
|
|
807
|
+
.filter((tool) => activeTools.has(tool.name))
|
|
808
|
+
.map(({ name, description, parameters }) => ({
|
|
809
|
+
name,
|
|
810
|
+
description,
|
|
811
|
+
parameters,
|
|
812
|
+
})),
|
|
813
|
+
raw,
|
|
814
|
+
);
|
|
815
|
+
const needed = shouldCompact(
|
|
816
|
+
tokens,
|
|
817
|
+
ctx.model.contextWindow,
|
|
818
|
+
event.preparation.settings,
|
|
819
|
+
);
|
|
820
|
+
logGoodiesEvent({
|
|
821
|
+
type: "side_compaction_check",
|
|
822
|
+
raw_tokens: event.preparation.tokensBefore,
|
|
823
|
+
side_tokens: tokens,
|
|
824
|
+
context_window: ctx.model.contextWindow,
|
|
825
|
+
reserve_tokens: event.preparation.settings.reserveTokens,
|
|
826
|
+
cancelled: !needed,
|
|
827
|
+
});
|
|
828
|
+
if (!needed) return { cancel: true };
|
|
829
|
+
});
|
|
830
|
+
|
|
630
831
|
// Lens: rewrite side-agent requests so the main trajectory arrives as an
|
|
631
832
|
// attributed quote instead of the side model's own history. Detection uses
|
|
632
833
|
// the raw branch (the marker is always an ancestor there); content uses
|
|
@@ -932,20 +1133,28 @@ export default function side(pi: ExtensionAPI): void {
|
|
|
932
1133
|
// mode (fail-closed) rather than exiting empty-handed.
|
|
933
1134
|
let summaryText: string | undefined;
|
|
934
1135
|
if (mode === "summary" && deltaTurns.length > 0) {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1136
|
+
const result = await summarizeWithProgress(
|
|
1137
|
+
ctx,
|
|
1138
|
+
effectiveSide,
|
|
1139
|
+
renderTrajectory(deltaTurns),
|
|
1140
|
+
deltaTurns.length,
|
|
1141
|
+
);
|
|
1142
|
+
if (!result.ok) {
|
|
1143
|
+
if (result.cancelled) {
|
|
1144
|
+
ctx.ui.notify(
|
|
1145
|
+
"side-exit: summarization cancelled — still in the side session",
|
|
1146
|
+
"warning",
|
|
1147
|
+
);
|
|
1148
|
+
} else {
|
|
1149
|
+
logGoodiesEvent({
|
|
1150
|
+
type: "side_summary_failed",
|
|
1151
|
+
error: describeError(result.error),
|
|
1152
|
+
});
|
|
1153
|
+
ctx.ui.notify(`side-exit: ${describeError(result.error)}`, "error");
|
|
1154
|
+
}
|
|
947
1155
|
return;
|
|
948
1156
|
}
|
|
1157
|
+
summaryText = result.text;
|
|
949
1158
|
}
|
|
950
1159
|
|
|
951
1160
|
const result = await ctx.navigateTree(marker.data.mainTipId, {
|
package/zed.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /z — Open Zed editor on cwd.
|
|
3
|
-
*/
|
|
4
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
export default function (pi: ExtensionAPI) {
|
|
8
|
-
// Arch packages the CLI as "zeditor"
|
|
9
|
-
const zedBin = process.platform === "linux" ? "zeditor" : "zed";
|
|
10
|
-
|
|
11
|
-
pi.registerCommand("z", {
|
|
12
|
-
description: "Open Zed editor on cwd (new window)",
|
|
13
|
-
handler: async (_args, ctx) => {
|
|
14
|
-
const child = spawn(zedBin, ["--new", ctx.cwd], {
|
|
15
|
-
detached: true,
|
|
16
|
-
stdio: "ignore",
|
|
17
|
-
});
|
|
18
|
-
child.on("error", (err) => {
|
|
19
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
20
|
-
if (code === "ENOENT") {
|
|
21
|
-
ctx.ui.notify(
|
|
22
|
-
`Zed not found: \`${zedBin}\` is not installed or not on PATH`,
|
|
23
|
-
"error",
|
|
24
|
-
);
|
|
25
|
-
} else {
|
|
26
|
-
ctx.ui.notify(`Failed to open Zed: ${err.message}`, "error");
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
// Only announce once the process has actually started — spawn()'s
|
|
30
|
-
// 'error' event fires asynchronously, so notifying synchronously would
|
|
31
|
-
// show "Opening Zed" before a missing-binary error lands.
|
|
32
|
-
child.on("spawn", () => {
|
|
33
|
-
ctx.ui.notify(`Opening Zed: ${ctx.cwd}`, "info");
|
|
34
|
-
});
|
|
35
|
-
child.unref();
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
}
|