pi-midcompact 0.5.0 → 0.5.2

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
@@ -29,7 +29,7 @@ That is planning guidance, not an enforced token target: semantic importance win
29
29
 
30
30
  ### A selective projection, prepared on a temporary branch
31
31
 
32
- `/midcompact start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the draft never becomes part of the committed working context.
32
+ `/midcompact:start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the draft never becomes part of the committed working context.
33
33
 
34
34
  ```text
35
35
  Frozen anchor: raw session history
@@ -42,7 +42,7 @@ Planning is isolated on a temporary branch:
42
42
  ... [latest work] ──┬── [transaction] ── [draft v1] ── [draft v2] ◀ review / edit
43
43
  │ (abandoned at commit)
44
44
  └── [midcompact-state] ◀ committed leaf
45
- (reviewed selection metadata, not a model message; written only by /midcompact commit)
45
+ (reviewed selection metadata, not a model message; written only by /midcompact:commit)
46
46
 
47
47
  Later model requests see a selective projection:
48
48
 
@@ -91,10 +91,10 @@ pi-midcompact — mid-context compression, review, then human commit
91
91
 
92
92
  | | Pi `/compact` | `pi-midcompact` |
93
93
  | --- | --- | --- |
94
- | Starts | Automatically near the context limit, or with `/compact` | At an explicit natural checkpoint with `/midcompact start` |
94
+ | Starts | Automatically near the context limit, or with `/compact` | At an explicit natural checkpoint with `/midcompact:start` |
95
95
  | Selects | One older contiguous prefix; keeps a recent token-budgeted tail | One or more reviewed ranges, including non-contiguous ranges and `KEEP` holes |
96
96
  | Planning | Optional one-shot instruction to focus the generated summary | User states scope and retention depth; the Agent discusses trade-offs and drafts selective ranges and summaries |
97
- | Decision gate | Generates a compaction checkpoint directly | Draft → TUI or browser review → explicit human `/midcompact commit` |
97
+ | Decision gate | Generates a compaction checkpoint directly | Draft → TUI or browser review → explicit human `/midcompact:commit` |
98
98
  | Best fit | Automatic context maintenance and overflow recovery | Deliberate cleanup of completed phases while retaining specific decisions verbatim |
99
99
 
100
100
  `pi-midcompact` does not disable or replace Pi's automatic compaction; it gives you a separate, human-reviewed way to make selective reductions. See [Pi's compaction documentation](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/compaction.md) for the built-in mechanism.
@@ -124,13 +124,13 @@ Start a transaction at a natural breakpoint: the current work is complete enough
124
124
  Run:
125
125
 
126
126
  ```text
127
- /midcompact start
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
- /midcompact start Compress the early repository exploration, but keep user requirements verbatim.
133
+ /midcompact:start Compress the early repository exploration, but keep user requirements verbatim.
134
134
  ```
135
135
 
136
136
  ### 2. Discuss what to compress with the Agent
@@ -150,7 +150,7 @@ The Agent locates relevant parts of the frozen conversation, proposes one or mor
150
150
  Run:
151
151
 
152
152
  ```text
153
- /midcompact review
153
+ /midcompact:review
154
154
  ```
155
155
 
156
156
  In interactive mode, the native TUI displays the frozen conversation as a linear timeline. Each item is marked either `KEEP` or as belonging to a proposed range. Review the range boundaries and the summary that will replace each range.
@@ -158,14 +158,14 @@ In interactive mode, the native TUI displays the frozen conversation as a linear
158
158
  For RPC, print, or other no-TUI modes, use the editable local browser interface instead:
159
159
 
160
160
  ```text
161
- /midcompact review-webui
161
+ /midcompact:review-webui
162
162
  ```
163
163
 
164
164
  Use Selection to create or change ranges and KEEP holes:
165
165
 
166
166
  ```text
167
- /midcompact select
168
- /midcompact select-webui
167
+ /midcompact:select
168
+ /midcompact:select-webui
169
169
  ```
170
170
 
171
171
  Use the TUI or local browser Review surface to edit summaries/topics and reject ranges. Review deliberately does not create or resize ranges; reopen Selection for boundary changes. To continue with Agent after a user-created plan, send a normal message asking it to continue the current midcompact draft; the Agent is prompted to read the existing plan first.
@@ -175,7 +175,7 @@ Use the TUI or local browser Review surface to edit summaries/topics and reject
175
175
  When the plan is correct, run:
176
176
 
177
177
  ```text
178
- /midcompact commit
178
+ /midcompact:commit
179
179
  ```
180
180
 
181
181
  This is deliberately a human command. The Agent cannot commit compression itself.
@@ -187,14 +187,14 @@ Pi returns to the anchor, discards the temporary planning branch, stores the rev
187
187
  Keep working normally after committing. If you decide not to compress, run:
188
188
 
189
189
  ```text
190
- /midcompact abort
190
+ /midcompact:abort
191
191
  ```
192
192
 
193
193
  This returns to the anchor and discards the transaction without changing the active context.
194
194
 
195
195
  ## Native TUI Controls
196
196
 
197
- Inside `/midcompact review`:
197
+ Inside `/midcompact:review`:
198
198
 
199
199
  ```text
200
200
  n/p or Left/Right select a proposed range
@@ -211,14 +211,14 @@ 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. |
215
- | `/midcompact select` | Opens the native TUI Selection workbench for range and KEEP editing. |
216
- | `/midcompact select-webui` | Opens the local browser Selection workbench. |
217
- | `/midcompact review` | Opens summary/topic review in the native TUI. |
218
- | `/midcompact review-webui` | Opens summary/topic review in a local browser. |
219
- | `/midcompact commit` | Commits the reviewed draft. Human only. |
220
- | `/midcompact abort` | Abandons the transaction and returns to the anchor. |
221
- | `/midcompact status` | Displays the current draft, or the committed compression state on this branch. |
214
+ | `/midcompact:start [instructions]` | Opens Agent direct / User manual / Drop, then starts a transaction at the current session-tree leaf. |
215
+ | `/midcompact:select` | Opens the native TUI Selection workbench for range and KEEP editing. |
216
+ | `/midcompact:select-webui` | Opens the local browser Selection workbench. |
217
+ | `/midcompact:review` | Opens summary/topic review in the native TUI. |
218
+ | `/midcompact:review-webui` | Opens summary/topic review in a local browser. |
219
+ | `/midcompact:commit` | Commits the reviewed draft. Human only. |
220
+ | `/midcompact:abort` | Abandons the transaction and returns to the anchor. |
221
+ | `/midcompact:status` | Displays the current draft, or the committed compression state on this branch. |
222
222
 
223
223
  The extension shows planning status in Pi's footer only while a transaction is active. It disappears after commit or abort.
224
224
 
@@ -227,7 +227,7 @@ The extension shows planning status in Pi's footer only while a transaction is a
227
227
  - **Original history is retained.** Compression changes what later model requests see, not the stored Pi messages.
228
228
  - **Fail-open projection.** If an exact reviewed sequence no longer resolves, the extension sends the raw history unchanged rather than removing uncertain content.
229
229
  - **State is branch-local.** Navigating with `/tree` to a point before a committed state restores raw history; returning to its descendant restores the projection.
230
- - **Human review is required.** The Agent can propose a plan but cannot execute `/midcompact commit`.
230
+ - **Human review is required.** The Agent can propose a plan but cannot execute `/midcompact:commit`.
231
231
  - **Tool protocol is protected.** Unknown, incomplete, or orphaned tool exchanges are not compressible.
232
232
  - **Repeated transactions work.** Later transactions can compress newly accumulated raw context; existing summaries remain protected.
233
233
  - **Native Pi `/compact` interaction needs more real-session validation.** Avoid relying on mixed automatic/native compaction behavior for critical work until it has been exercised in your environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-midcompact",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Branch-aware mid-context compression for the Pi coding agent",
5
5
  "author": "frostime",
6
6
  "repository": {
@@ -18,9 +18,11 @@ This skill handles two independent tasks: planning compression and recalling com
18
18
 
19
19
  The state-specific runtime prompt is authoritative. Recall does not enter planning or mutate the DraftPlan. During the User-manual acknowledgement turn, the no-tool instruction overrides every other route.
20
20
 
21
+ The tool groups parameters by `action`: each action accepts only its own fields, and fields from other actions are rejected. Read `references/tool-interface.md` before adding workload-specific parameters, and treat the action-specific field lists there as exhaustive.
22
+
21
23
  ## Plan compression
22
24
 
23
- Selected ranges become summaries in future model context; content outside them stays verbatim. Originals remain stored, but recall is a recovery path, not a substitute for a sufficient summary. Agent and user edit one DraftPlan; the user retains final control through review and `/midcompact commit`.
25
+ Selected ranges become summaries in future model context; content outside them stays verbatim. Originals remain stored, but recall is a recovery path, not a substitute for a sufficient summary. Agent and user edit one DraftPlan; the user retains final control through review and `/midcompact:commit`.
24
26
 
25
27
  The start mode controls how the first draft is created. It does not determine whether a user selection is final, how much initiative the Agent should take, or how deeply the history should be investigated.
26
28
 
@@ -107,7 +109,7 @@ Length follows the information that must survive, not a target ratio. Final test
107
109
 
108
110
  Call `action="plan", op="show"`. Check that the intended semantic phases are covered, KEEP holes remain outside ranges, every range has a summary, and the summaries conserve the future working state.
109
111
 
110
- Describe the completed proposal with the same recognizable landmarks used during alignment. Direct the user to `/midcompact select` or `/midcompact select-webui` for boundaries and KEEP holes, and to `/midcompact review` or `/midcompact review-webui` for summary inspection or rejection. Use browser variants when the TUI is unavailable or preferred. Ask the user to run `/midcompact commit` when ready; never commit for them.
112
+ Describe the completed proposal with the same recognizable landmarks used during alignment. Direct the user to `/midcompact:select` or `/midcompact:select-webui` for boundaries and KEEP holes, and to `/midcompact:review` or `/midcompact:review-webui` for summary inspection or rejection. Use browser variants when the TUI is unavailable or preferred. Ask the user to run `/midcompact:commit` when ready; never commit for them.
111
113
 
112
114
  ## Recall compressed content
113
115
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  Read this reference when exact call requirements, limits, rejection behavior, repeated compression, or recall truncation matters. The main workflow remains in `../SKILL.md`.
4
4
 
5
+ ## Parameter grouping
6
+
7
+ The tool schema is a discriminated union on `action`: selecting an action admits exactly the fields documented in that action's section, and no fields from other actions. A call that mixes actions (for example `locate` fields on `plan`) is invalid; do not repair it by dropping fields, re-issue the call with only the selected action's parameters. Shared field names (`ref`, `pattern`, `limit`, `detail`) are defined independently per action with the meaning documented in that section.
8
+
5
9
  ## Inspect
6
10
 
7
11
  Without `spans`, `action="inspect"` inventories the frozen anchor. It returns factual structure and bounded user landmarks, not full message bodies, assistant/tool previews, summaries, or image base64.
package/src/index.ts CHANGED
@@ -6,8 +6,6 @@ import {
6
6
  type ExtensionContext,
7
7
  type SessionEntry,
8
8
  } from "@earendil-works/pi-coding-agent";
9
- import type { AutocompleteItem } from "@earendil-works/pi-tui";
10
-
11
9
  import { buildAtoms, formatLocatedAtom, isProtectedAtom, locateAtomMatches } from "./atoms.js";
12
10
  import { buildInventory, formatInventory, formatSpanInspection } from "./inventory.js";
13
11
  import { messageText } from "./messages.js";
@@ -17,8 +15,20 @@ import { projectMessages } from "./projection.js";
17
15
  import { registerStateRenderer, stateTreeLabel } from "./renderers.js";
18
16
  import { showReviewUi } from "./review-ui.js";
19
17
  import { showSelectionUi } from "./selection-ui.js";
20
- import { showStartChoiceUi } from "./start-ui.js";
18
+ import { showStartChoice } from "./start-ui.js";
21
19
  import { showReviewWebUi } from "./review-webui.js";
20
+
21
+ /**
22
+ * Browser opener for the web workbenches. Tests override it to keep the suites
23
+ * from spawning a system browser; production leaves it undefined and opens the
24
+ * default browser.
25
+ */
26
+ export let openReviewWebBrowser: ((url: string) => void) | undefined;
27
+
28
+ /** Test seam setter; see `openReviewWebBrowser`. */
29
+ export function setOpenReviewWebBrowser(opener?: (url: string) => void): void {
30
+ openReviewWebBrowser = opener;
31
+ }
22
32
  import {
23
33
  DRAFT_ENTRY,
24
34
  STATE_ENTRY,
@@ -57,8 +67,20 @@ const TOOL_DESCRIPTION =
57
67
  const STATUS_KEY = "midcompact";
58
68
  const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen anchor snapshot.";
59
69
 
60
- const Params = Type.Object({
61
- action: StringEnum(["inspect", "locate", "plan", "recall"] as const),
70
+ // The tool is a discriminated union: `action` selects the only parameter
71
+ // group that applies. The schema exposes just that group's fields, so a call
72
+ // cannot mix parameters from different actions, and cross-action misuse
73
+ // becomes a compile-time error in the handler types rather than a runtime rule.
74
+ const InspectParams = Type.Object({
75
+ action: Type.Literal("inspect"),
76
+ // inventory pagination or explicit candidate-span measurement
77
+ spans: Type.Optional(Type.Array(Type.Object({ start: Type.String(), end: Type.String() }))),
78
+ page_size: Type.Optional(Type.Number()),
79
+ cursor: Type.Optional(Type.String()),
80
+ });
81
+
82
+ const LocateParams = Type.Object({
83
+ action: Type.Literal("locate"),
62
84
  ref: Type.Optional(Type.String()),
63
85
  pattern: Type.Optional(Type.String()),
64
86
  source: Type.Optional(StringEnum(["any", "user", "assistant", "tool_call", "tool_result"] as const)),
@@ -66,19 +88,34 @@ const Params = Type.Object({
66
88
  direction: Type.Optional(StringEnum(["oldest", "newest"] as const)),
67
89
  limit: Type.Optional(Type.Number()),
68
90
  detail: Type.Optional(StringEnum(["brief", "full"] as const)),
91
+ });
92
+
93
+ const PlanParams = Type.Object({
94
+ action: Type.Literal("plan"),
69
95
  op: Type.Optional(StringEnum(["show", "add", "update", "remove"] as const)),
70
96
  start: Type.Optional(Type.String()),
71
97
  end: Type.Optional(Type.String()),
72
98
  draft_id: Type.Optional(Type.String()),
73
99
  topic: Type.Optional(Type.String()),
74
100
  summary: Type.Optional(Type.String()),
75
- // inspect inventory pagination or explicit candidate-span measurement
76
- page_size: Type.Optional(Type.Number()),
77
- cursor: Type.Optional(Type.String()),
78
- spans: Type.Optional(Type.Array(Type.Object({ start: Type.String(), end: Type.String() }))),
101
+ detail: Type.Optional(StringEnum(["brief", "full"] as const)),
79
102
  });
80
103
 
104
+ const RecallParams = Type.Object({
105
+ action: Type.Literal("recall"),
106
+ ref: Type.Optional(Type.String()),
107
+ pattern: Type.Optional(Type.String()),
108
+ limit: Type.Optional(Type.Number()),
109
+ detail: Type.Optional(StringEnum(["brief", "full"] as const)),
110
+ });
111
+
112
+ const Params = Type.Union([InspectParams, LocateParams, PlanParams, RecallParams]);
113
+
81
114
  type ParamsType = Static<typeof Params>;
115
+ type InspectParamsType = Static<typeof InspectParams>;
116
+ type LocateParamsType = Static<typeof LocateParams>;
117
+ type PlanParamsType = Static<typeof PlanParams>;
118
+ type RecallParamsType = Static<typeof RecallParams>;
82
119
 
83
120
  type RuntimeSnapshot = { atoms: Atom[]; anchorState?: CompressionState };
84
121
 
@@ -160,48 +197,63 @@ export default function (pi: ExtensionAPI) {
160
197
  return { messages: projectMessages(event.messages as MessageLike[], activeState) as typeof event.messages };
161
198
  });
162
199
 
163
- pi.registerCommand("midcompact", {
164
- description: "Start, select, review, commit, inspect, or abort a branch-isolated mid-context compression transaction",
165
- getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
166
- const query = prefix.trimStart().toLowerCase();
167
- if (/\s/.test(query)) return null;
168
- const items: AutocompleteItem[] = [
169
- { value: "start", label: "start — Start a new midcompact transaction at the current anchor" },
170
- { value: "abort", label: "abort — Abort the active transaction and return to anchor" },
171
- { value: "commit", label: "commit — Commit the current draft to the branch state" },
172
- { value: "review", label: "review — Open interactive TUI to inspect and edit the draft" },
173
- { value: "review-webui", label: "review-webui — Open a local web page to inspect and edit the draft (works without TUI)" },
174
- { value: "select", label: "select — Open the TUI Selection surface to edit range boundaries" },
175
- { value: "select-webui", label: "select-webui — Open Selection in a local browser" },
176
- { value: "status", label: "status — Show current transaction and draft status" },
177
- ];
178
- const filtered = items.filter((item) => item.value.startsWith(query));
179
- return filtered.length > 0 ? filtered : null;
180
- },
200
+ // One command per operation; the `midcompact:` prefix follows the same
201
+ // naming convention as Pi's own `skill:<name>` commands. The former
202
+ // composite `/midcompact <subcommand>` command is gone.
203
+ pi.registerCommand("midcompact:start", {
204
+ description: "Start a new midcompact transaction at the current anchor. Optional trailing text becomes the initial focus.",
181
205
  handler: async (args: string, ctx: ExtensionCommandContext) => {
182
206
  await ctx.waitForIdle();
183
- const rawArgs = args.trim();
184
- const lower = rawArgs.toLowerCase();
185
-
186
- if (lower === "abort") return abortTransaction(ctx);
187
- if (lower === "commit") return commitTransaction(ctx);
188
- if (lower === "review") return reviewTransaction(ctx, "tui");
189
- if (lower === "review-webui") return reviewTransaction(ctx, "web");
190
- if (lower === "select") return openSelectionUi(ctx, "auto");
191
- if (lower === "select-webui") return openSelectionUi(ctx, "web");
192
- if (lower === "status") return showStatus(ctx);
193
-
194
- // start [instructions...]
195
- const startMatch = rawArgs.match(/^start\b\s*(.*)$/i);
196
- if (startMatch) {
197
- const instructions = (startMatch[1] ?? "").trim();
198
- return startTransaction(ctx, instructions || undefined);
199
- }
200
-
201
- ctx.ui.notify(
202
- "Usage: /midcompact start [instructions] | /midcompact select[-webui] | /midcompact review[-webui] | /midcompact commit | /midcompact status | /midcompact abort",
203
- "warning",
204
- );
207
+ return startTransaction(ctx, args.trim() || undefined);
208
+ },
209
+ });
210
+ pi.registerCommand("midcompact:abort", {
211
+ description: "Abort the active transaction and return to the anchor",
212
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
213
+ await ctx.waitForIdle();
214
+ return abortTransaction(ctx);
215
+ },
216
+ });
217
+ pi.registerCommand("midcompact:commit", {
218
+ description: "Commit the current draft to the branch state",
219
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
220
+ await ctx.waitForIdle();
221
+ return commitTransaction(ctx);
222
+ },
223
+ });
224
+ pi.registerCommand("midcompact:review", {
225
+ description: "Open the interactive TUI review to inspect and edit the draft",
226
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
227
+ await ctx.waitForIdle();
228
+ return reviewTransaction(ctx, "tui");
229
+ },
230
+ });
231
+ pi.registerCommand("midcompact:review-webui", {
232
+ description: "Open a local web page to inspect and edit the draft (works without TUI)",
233
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
234
+ await ctx.waitForIdle();
235
+ return reviewTransaction(ctx, "web");
236
+ },
237
+ });
238
+ pi.registerCommand("midcompact:select", {
239
+ description: "Open the TUI Selection surface to edit range boundaries",
240
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
241
+ await ctx.waitForIdle();
242
+ return openSelectionUi(ctx, "auto");
243
+ },
244
+ });
245
+ pi.registerCommand("midcompact:select-webui", {
246
+ description: "Open Selection in a local browser",
247
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
248
+ await ctx.waitForIdle();
249
+ return openSelectionUi(ctx, "web");
250
+ },
251
+ });
252
+ pi.registerCommand("midcompact:status", {
253
+ description: "Show current transaction and draft status",
254
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
255
+ await ctx.waitForIdle();
256
+ return showStatus(ctx);
205
257
  },
206
258
  });
207
259
 
@@ -218,23 +270,18 @@ export default function (pi: ExtensionAPI) {
218
270
  ctx.ui.notify("Cannot start midcompact without a session leaf.", "error");
219
271
  return;
220
272
  }
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
273
  // Mode choice: Agent-first or User-first. Both operate on the same DraftPlan;
232
- // neither freezes boundaries. No mode flags on the command line.
274
+ // neither freezes boundaries. Drop maps to cancellation, so this one chooser
275
+ // replaces the separate confirmation. No mode flags on the command line.
233
276
  const modeChoice = await chooseStartMode(ctx);
234
277
  if (modeChoice === "cancelled") {
235
278
  ctx.ui.notify("Midcompact start cancelled.", "info");
236
279
  return;
237
280
  }
281
+ if (modeChoice === "unrecognized") {
282
+ ctx.ui.notify("Midcompact start cancelled: the dialog returned an unrecognized choice.", "warning");
283
+ return;
284
+ }
238
285
  const startMode: StartMode = modeChoice;
239
286
  transaction = {
240
287
  version: 1,
@@ -261,9 +308,11 @@ export default function (pi: ExtensionAPI) {
261
308
  await openSelectionUi(ctx);
262
309
  }
263
310
 
264
- async function chooseStartMode(ctx: ExtensionCommandContext): Promise<StartMode | "cancelled"> {
265
- if (!ctx.hasUI || ctx.mode !== "tui") return "agent";
266
- return showStartChoiceUi(ctx);
311
+ async function chooseStartMode(ctx: ExtensionCommandContext): Promise<StartMode | "cancelled" | "unrecognized"> {
312
+ // The standard `select` dialog works identically in TUI and RPC; modes
313
+ // without any UI (json/print) cannot prompt and default to Agent-first.
314
+ if (!ctx.hasUI) return "agent";
315
+ return showStartChoice(ctx);
267
316
  }
268
317
 
269
318
  async function openSelectionUi(ctx: ExtensionCommandContext, mode: "auto" | "tui" | "web" = "auto"): Promise<void> {
@@ -312,7 +361,7 @@ export default function (pi: ExtensionAPI) {
312
361
  pi.appendEntry(DRAFT_ENTRY, draft);
313
362
  updateStatus(ctx, currentTx, draft, planningLock.owner);
314
363
  },
315
- }, "selection");
364
+ }, "selection", { openBrowser: openReviewWebBrowser });
316
365
  ctx.ui.notify("Selection closed. The DraftPlan is saved; tell the Agent to continue when ready.", "info");
317
366
  } finally {
318
367
  releaseUi(planningLock);
@@ -457,14 +506,14 @@ export default function (pi: ExtensionAPI) {
457
506
  editSummary: (id, summary) => commitMutation(updateDraftRange(draft ?? emptyDraft(currentTx.id), id, { summary })),
458
507
  editTopic: (id, topic) => commitMutation(updateDraftRange(draft ?? emptyDraft(currentTx.id), id, { topic: topic || undefined })),
459
508
  remove: (id) => commitMutation(removeDraftRange(draft ?? emptyDraft(currentTx.id), id)),
460
- });
509
+ }, undefined, { openBrowser: openReviewWebBrowser });
461
510
  ctx.ui.notify("Midcompact review-webui closed.", "info");
462
511
  return;
463
512
  }
464
513
 
465
514
  if (ctx.mode !== "tui") {
466
515
  ctx.ui.notify(
467
- "Interactive TUI review is only available in interactive (tui) mode. Use /midcompact review-webui to open a local web page instead.",
516
+ "Interactive TUI review is only available in interactive (tui) mode. Use /midcompact:review-webui to open a local web page instead.",
468
517
  "warning",
469
518
  );
470
519
  return;
@@ -529,7 +578,7 @@ export default function (pi: ExtensionAPI) {
529
578
  if (params.action === "recall") return toolResult(handleRecall(params, ctx));
530
579
  const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
531
580
  const currentTx = withCompatDefaults(restored.transaction ?? transaction);
532
- if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact start` first.");
581
+ if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact:start` first.");
533
582
  transaction = currentTx;
534
583
  draft = restored.draft ? { ...restored.draft, ranges: restored.draft.ranges.map(coerceDraftRange) } : (draft ?? emptyDraft(currentTx.id));
535
584
  if (!requireAgentAccess(ctx)) {
@@ -560,7 +609,7 @@ export default function (pi: ExtensionAPI) {
560
609
  },
561
610
  });
562
611
 
563
- function handleInspect(params: ParamsType, atoms: Atom[], tx: TransactionState): string {
612
+ function handleInspect(params: InspectParamsType, atoms: Atom[], tx: TransactionState): string {
564
613
  if (params.spans) {
565
614
  if (params.page_size !== undefined || params.cursor !== undefined) {
566
615
  throw new Error("inspect spans cannot be combined with inventory pagination.");
@@ -571,7 +620,7 @@ export default function (pi: ExtensionAPI) {
571
620
  return formatInventory(page);
572
621
  }
573
622
 
574
- function handleRecall(params: ParamsType, ctx: ExtensionContext): string {
623
+ function handleRecall(params: RecallParamsType, ctx: ExtensionContext): string {
575
624
  const sm = ctx.sessionManager;
576
625
  const branchState = restoreCompressionState(sm.getBranch() as SessionEntry[]) ?? activeState;
577
626
  if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
@@ -607,7 +656,7 @@ export default function (pi: ExtensionAPI) {
607
656
 
608
657
  // ---- Pure handlers ----
609
658
 
610
- function handleLocate(params: ParamsType, atoms: Atom[]): string {
659
+ function handleLocate(params: LocateParamsType, atoms: Atom[]): string {
611
660
  const hasFilter = Boolean(params.pattern || params.tool_name || (params.source && params.source !== "any"));
612
661
  if (params.ref && hasFilter) {
613
662
  throw new Error("locate accepts either one direct ref or search filters, not both.");
@@ -639,7 +688,7 @@ type PlanHandleResult =
639
688
  | { op: "show"; draft: DraftPlan }
640
689
  | { op: "add" | "update" | "remove"; draft: DraftPlan; changedId: string };
641
690
 
642
- function handlePlan(params: ParamsType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
691
+ function handlePlan(params: PlanParamsType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
643
692
  const op = params.op ?? "show";
644
693
  if (op === "show") return { op, draft: current };
645
694
  if (op === "remove") {
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
+ }
package/src/types.ts CHANGED
@@ -178,7 +178,7 @@ export interface TransactionState {
178
178
  startedAt: string;
179
179
  /** Initial routing chosen at start: Agent-first prompt or User-first UI. */
180
180
  startMode?: StartMode;
181
- /** Frozen awareness captured when /midcompact starts; informational, never a target. */
181
+ /** Frozen awareness captured when /midcompact:start runs; informational, never a target. */
182
182
  anchorUsage?: ContextUsageSnapshot;
183
183
  }
184
184