pi-midcompact 0.5.0 → 0.5.1

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 CHANGED
@@ -127,7 +127,7 @@ Run:
127
127
  /midcompact start
128
128
  ```
129
129
 
130
- Pi opens a three-way chooser before creating transaction state: **Drop**, **Agent direct**, or **User manual**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens. It does not start planning or mutate the DraftPlan until the user hands off later. Save the initial DraftPlan, close the UI, then tell the Agent to continue when you are ready. You can include an initial focus in the same command:
130
+ Pi opens a three-way chooser before creating transaction state: **Agent direct**, **User manual**, or **Drop**. Agent direct is the first and default-highlighted option, matching the previous fast path. The chooser is the standard `select` dialog, identical in interactive and RPC mode; RPC carries it as an extension UI `select` message with a bounded timeout, so an unresponsive client cancels instead of blocking. print/JSON modes have no dialog and default to **Agent direct**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens (browser-based outside interactive mode). It does not start planning or mutate the DraftPlan until the user hands off later. Save the initial DraftPlan, close the UI, then tell the Agent to continue when you are ready. You can include an initial focus in the same command:
131
131
 
132
132
  ```text
133
133
  /midcompact start Compress the early repository exploration, but keep user requirements verbatim.
@@ -211,7 +211,7 @@ Enter/Esc/q close
211
211
 
212
212
  | Command | Result |
213
213
  | --- | --- |
214
- | `/midcompact start [instructions]` | Opens Drop / Agent direct / User manual, then starts a transaction at the current session-tree leaf. |
214
+ | `/midcompact start [instructions]` | Opens Agent direct / User manual / Drop, then starts a transaction at the current session-tree leaf. |
215
215
  | `/midcompact select` | Opens the native TUI Selection workbench for range and KEEP editing. |
216
216
  | `/midcompact select-webui` | Opens the local browser Selection workbench. |
217
217
  | `/midcompact review` | Opens summary/topic review in the native TUI. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-midcompact",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Branch-aware mid-context compression for the Pi coding agent",
5
5
  "author": "frostime",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -17,8 +17,20 @@ import { projectMessages } from "./projection.js";
17
17
  import { registerStateRenderer, stateTreeLabel } from "./renderers.js";
18
18
  import { showReviewUi } from "./review-ui.js";
19
19
  import { showSelectionUi } from "./selection-ui.js";
20
- import { showStartChoiceUi } from "./start-ui.js";
20
+ import { showStartChoice } from "./start-ui.js";
21
21
  import { showReviewWebUi } from "./review-webui.js";
22
+
23
+ /**
24
+ * Browser opener for the web workbenches. Tests override it to keep the suites
25
+ * from spawning a system browser; production leaves it undefined and opens the
26
+ * default browser.
27
+ */
28
+ export let openReviewWebBrowser: ((url: string) => void) | undefined;
29
+
30
+ /** Test seam setter; see `openReviewWebBrowser`. */
31
+ export function setOpenReviewWebBrowser(opener?: (url: string) => void): void {
32
+ openReviewWebBrowser = opener;
33
+ }
22
34
  import {
23
35
  DRAFT_ENTRY,
24
36
  STATE_ENTRY,
@@ -218,23 +230,18 @@ export default function (pi: ExtensionAPI) {
218
230
  ctx.ui.notify("Cannot start midcompact without a session leaf.", "error");
219
231
  return;
220
232
  }
221
- if (ctx.hasUI && ctx.mode !== "tui") {
222
- const ok = await ctx.ui.confirm(
223
- "Start midcompact transaction?",
224
- "The current context snapshot will be frozen as an anchor. You will need to review and explicitly commit or abort later.",
225
- );
226
- if (!ok) {
227
- ctx.ui.notify("Midcompact start cancelled.", "info");
228
- return;
229
- }
230
- }
231
233
  // Mode choice: Agent-first or User-first. Both operate on the same DraftPlan;
232
- // neither freezes boundaries. No mode flags on the command line.
234
+ // neither freezes boundaries. Drop maps to cancellation, so this one chooser
235
+ // replaces the separate confirmation. No mode flags on the command line.
233
236
  const modeChoice = await chooseStartMode(ctx);
234
237
  if (modeChoice === "cancelled") {
235
238
  ctx.ui.notify("Midcompact start cancelled.", "info");
236
239
  return;
237
240
  }
241
+ if (modeChoice === "unrecognized") {
242
+ ctx.ui.notify("Midcompact start cancelled: the dialog returned an unrecognized choice.", "warning");
243
+ return;
244
+ }
238
245
  const startMode: StartMode = modeChoice;
239
246
  transaction = {
240
247
  version: 1,
@@ -261,9 +268,11 @@ export default function (pi: ExtensionAPI) {
261
268
  await openSelectionUi(ctx);
262
269
  }
263
270
 
264
- async function chooseStartMode(ctx: ExtensionCommandContext): Promise<StartMode | "cancelled"> {
265
- if (!ctx.hasUI || ctx.mode !== "tui") return "agent";
266
- return showStartChoiceUi(ctx);
271
+ async function chooseStartMode(ctx: ExtensionCommandContext): Promise<StartMode | "cancelled" | "unrecognized"> {
272
+ // The standard `select` dialog works identically in TUI and RPC; modes
273
+ // without any UI (json/print) cannot prompt and default to Agent-first.
274
+ if (!ctx.hasUI) return "agent";
275
+ return showStartChoice(ctx);
267
276
  }
268
277
 
269
278
  async function openSelectionUi(ctx: ExtensionCommandContext, mode: "auto" | "tui" | "web" = "auto"): Promise<void> {
@@ -312,7 +321,7 @@ export default function (pi: ExtensionAPI) {
312
321
  pi.appendEntry(DRAFT_ENTRY, draft);
313
322
  updateStatus(ctx, currentTx, draft, planningLock.owner);
314
323
  },
315
- }, "selection");
324
+ }, "selection", { openBrowser: openReviewWebBrowser });
316
325
  ctx.ui.notify("Selection closed. The DraftPlan is saved; tell the Agent to continue when ready.", "info");
317
326
  } finally {
318
327
  releaseUi(planningLock);
@@ -457,7 +466,7 @@ export default function (pi: ExtensionAPI) {
457
466
  editSummary: (id, summary) => commitMutation(updateDraftRange(draft ?? emptyDraft(currentTx.id), id, { summary })),
458
467
  editTopic: (id, topic) => commitMutation(updateDraftRange(draft ?? emptyDraft(currentTx.id), id, { topic: topic || undefined })),
459
468
  remove: (id) => commitMutation(removeDraftRange(draft ?? emptyDraft(currentTx.id), id)),
460
- });
469
+ }, undefined, { openBrowser: openReviewWebBrowser });
461
470
  ctx.ui.notify("Midcompact review-webui closed.", "info");
462
471
  return;
463
472
  }
package/src/start-ui.ts CHANGED
@@ -1,57 +1,52 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
2
 
4
3
  import type { StartMode } from "./types.js";
5
4
 
6
- export type StartChoice = StartMode | "cancelled";
5
+ export type StartChoice = StartMode | "cancelled" | "unrecognized";
7
6
 
8
- export async function showStartChoiceUi(ctx: ExtensionCommandContext): Promise<StartChoice> {
9
- if (ctx.mode !== "tui") return "agent";
10
- return ctx.ui.custom<StartChoice>(
11
- (tui, theme, _keybindings, done) => {
12
- const choices: Array<{ value: StartChoice; title: string; detail: string }> = [
13
- { value: "cancelled", title: "Drop", detail: "Leave the session unchanged" },
14
- { value: "agent", title: "Agent direct", detail: "Inspect and draft with Agent" },
15
- { value: "user", title: "User manual", detail: "Select the initial DraftPlan yourself" },
16
- ];
17
- let selected = 1;
18
- const component = {
19
- render(width: number): string[] {
20
- const w = Math.max(48, Math.min(width, 94));
21
- const line = (text: string) => theme.fg("border", `| ${truncateToWidth(text, w - 4, "...", true).padEnd(w - 4)} |`);
22
- const rule = theme.fg("border", `+${"-".repeat(w - 2)}+`);
23
- const rows = choices.map((choice, index) => {
24
- const marker = index === selected ? theme.fg("accent", ">") : " ";
25
- const rawTitle = choice.title.padEnd(18);
26
- const title = index === selected ? theme.fg("accent", theme.bold(rawTitle)) : rawTitle;
27
- return line(`${marker} ${String(index + 1)} ${title} ${theme.fg("dim", choice.detail)}`);
28
- });
29
- return [
30
- rule,
31
- line(theme.fg("accent", theme.bold("Midcompact | Freeze current context"))),
32
- line(theme.fg("dim", "Choose how to enter. Only Agent direct starts a model turn.")),
33
- rule,
34
- ...rows,
35
- rule,
36
- line(theme.fg("dim", "left/right or j/k move | Enter choose | Esc drop")),
37
- rule,
38
- ];
39
- },
40
- invalidate(): void {},
41
- handleInput(data: string): void {
42
- if (matchesKey(data, Key.escape) || data === "q") { done("cancelled"); return; }
43
- if (matchesKey(data, Key.enter)) { done(choices[selected]!.value); return; }
44
- if (data === "1") { done("cancelled"); return; }
45
- if (data === "2") { done("agent"); return; }
46
- if (data === "3") { done("user"); return; }
47
- if (matchesKey(data, Key.left) || matchesKey(data, Key.up) || data === "k") selected = (selected + choices.length - 1) % choices.length;
48
- else if (matchesKey(data, Key.right) || matchesKey(data, Key.down) || data === "j") selected = (selected + 1) % choices.length;
49
- else return;
50
- tui.requestRender();
51
- },
52
- };
53
- return component;
54
- },
55
- { overlay: true, overlayOptions: { width: "76%", maxHeight: 13, anchor: "center" } },
56
- );
7
+ interface StartChoiceOption {
8
+ value: StartChoice;
9
+ label: string;
57
10
  }
11
+
12
+ /**
13
+ * The three entry decisions. Shared by TUI and RPC so every mode offers the
14
+ * same choice set in the same order. Agent direct comes first: the standard
15
+ * selector highlights the first option, so Enter on open keeps the fast
16
+ * Agent-first start path, and RPC clients see the recommended entry first.
17
+ */
18
+ const START_CHOICES: StartChoiceOption[] = [
19
+ { value: "agent", label: "Agent direct — Inspect and draft with Agent" },
20
+ { value: "user", label: "User manual — Select the initial DraftPlan yourself" },
21
+ { value: "cancelled", label: "Drop — Leave the session unchanged" },
22
+ ];
23
+
24
+ /**
25
+ * Bound for RPC dialog waits. The timeout exists only as a safety net: when
26
+ * the RPC client never answers, the extension-UI sub-protocol auto-resolves
27
+ * the dialog (`undefined`) so the start cancels instead of blocking the
28
+ * extension forever. 120s is generous for the human on the other side of the
29
+ * wire (pi's own examples use 5s for urgent security confirms) while still
30
+ * bounding the worst-case stall. TUI mode passes no timeout: interactive
31
+ * users may take as long as they want.
32
+ */
33
+ const START_DIALOG_TIMEOUT_MS = 120_000;
34
+
35
+ /**
36
+ * Standard `select` dialog for the start mode, used identically in TUI and
37
+ * RPC. It serializes as an extension UI `select` request over the RPC
38
+ * sub-protocol, while interactive mode renders the built-in selector.
39
+ */
40
+ export async function showStartChoice(ctx: ExtensionCommandContext): Promise<StartChoice> {
41
+ const options = START_CHOICES.map((choice) => choice.label);
42
+ const choice = await ctx.ui.select(
43
+ "Midcompact start — freeze the current context as an anchor; review, then commit or abort explicitly",
44
+ options,
45
+ ctx.mode === "rpc" ? { timeout: START_DIALOG_TIMEOUT_MS } : undefined,
46
+ );
47
+ if (choice === undefined) return "cancelled";
48
+ // A value outside the offered options is a protocol mismatch on the client
49
+ // side, not a user decision; keep it distinguishable from a real Drop.
50
+ const index = options.indexOf(choice);
51
+ return index >= 0 ? START_CHOICES[index]!.value : "unrecognized";
52
+ }