@henryqw/pi-subagent 27.6.0 → 27.6.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
@@ -137,9 +137,9 @@ The extension sends follow-ups when a candidate becomes ready, a worker needs at
137
137
 
138
138
  ### Inspect work
139
139
 
140
- Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. **Refresh** reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
140
+ Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. The menu groups rows under **Direct**, **Isolated**, and **Orphaned** headers. Each isolated row shows the request ID, the status, and a short goal summary. **Refresh** or a header reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
141
141
 
142
- Requests whose parent checkout was removed show as **Isolated · orphaned** from any checkout of the same repository. Every isolated request, including an orphaned request, has **Close and delete worktrees (discard work)**. Select it to review owned resources and confirm deletion. **This permanently deletes committed, uncommitted, and ignored content in the listed checkouts.** Cancel makes no change.
142
+ Requests whose parent checkout was removed show in the **Orphaned · parent checkout removed** group from any checkout of the same repository. Every isolated request, including an orphaned request, has **Close and delete worktrees (discard work)**. Select it to review owned resources and confirm deletion. **This permanently deletes committed, uncommitted, and ignored content in the listed checkouts.** Cancel makes no change.
143
143
 
144
144
  Only shown resources can be removed; live jobs, uncertain ownership, or other Herdr panes can block removal. Agents and tools cannot trigger this action. See [Close and delete isolated worktrees](./docs/orchestration.md#close-and-delete-isolated-worktrees) for guards and partial cleanup.
145
145
 
@@ -271,7 +271,7 @@ After readiness, use `subagent_stage revise` with the exact candidate, generatio
271
271
 
272
272
  To select work by label, open `/subagent` without arguments. The menu shows direct records on this session branch and isolated requests in the current canonical Git checkout. This includes requests launched in other sessions. Select an isolated request. Then select **Inspect status and recovery**.
273
273
 
274
- The menu also shows **Isolated · orphaned** requests whose saved parent checkout no longer exists. You can open this view from any checkout of the same repository. The saved worker or integration path identifies the primary repository. Orphaned requests have the same **Close and delete worktrees (discard work)** action, but no inspection or follow-up actions. The extension reads their original state directories; it does not recreate the removed parent checkout. Unreadable files stay preserved and are reported.
274
+ The menu also shows requests whose saved parent checkout no longer exists, in their own **Orphaned · parent checkout removed** group. You can open this view from any checkout of the same repository. The saved worker or integration path identifies the primary repository. Orphaned requests have the same **Close and delete worktrees (discard work)** action, but no inspection or follow-up actions. The extension reads their original state directories; it does not recreate the removed parent checkout. Unreadable files stay preserved and are reported.
275
275
 
276
276
  This launches a Herdr tab without focus, with the configured `scout` Role on the configured `fast` route. The tab runs a read-access task with the resources of the Role. Its prompt contains bounded saved status, including retained paths, but not the raw launch environment or session transcripts. The analysis stays in that tab and does not queue a Main turn. The tab shows in the direct list with its own reference and name, such as `D9.1 · scout · Inspect <request ID>`. The extension records the tab and session file for recovery, even if the launch becomes uncertain.
277
277
 
@@ -101,6 +101,13 @@ export interface SubagentCommandAdapter {
101
101
  const clean = (text: string, max = 110, multiline = false): string => [...text.replace(multiline ? /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, " ")].slice(0, max).join("");
102
102
  const ROLES_REPORT_CODE_POINTS = 16_000;
103
103
  const errorText = (error: unknown): string => clean(error instanceof Error ? error.message : String(error), 500);
104
+ /** One-line summary of free text such as a goal; the ellipsis marks a cut. */
105
+ const summary = (text: string, max = 60): string => {
106
+ const chars = [...clean(text, Number.MAX_SAFE_INTEGER).replace(/\s+/g, " ").trim()];
107
+ return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : chars.join("");
108
+ };
109
+ // Identity and status first so clipping cuts only the goal summary.
110
+ const isolatedLabel = ({ id, status, name }: IsolatedRequest): string => `${id} · ${status} · ${summary(name)}`;
104
111
  const completed = (status: string) => ["completed", "success", "failed", "failure", "aborted", "integrated"].includes(status);
105
112
  const priority = (status: string) => /attention|blocked|retained|failed/i.test(status) ? 0 : completed(status) ? 2 : 1;
106
113
 
@@ -162,8 +169,9 @@ function discardReview(plan: DiscardPlan) {
162
169
 
163
170
  type Choice<T> = { label: string; value: T };
164
171
  async function choose<T>(ctx: ExtensionContext, title: string, items: readonly Choice<T>[]): Promise<T | undefined> {
165
- // Numeric suffixes are UI-only. Never parse display text into an identity.
166
- const labels = items.map((item, index) => `${clean(item.label, 145)} [${index + 1}]`);
172
+ // Numeric suffixes only tell duplicate labels apart. Never parse display text into an identity.
173
+ const clipped = items.map((item) => clean(item.label, 145));
174
+ const labels = clipped.map((label, index) => clipped.indexOf(label) === clipped.lastIndexOf(label) ? label : `${label} [${index + 1}]`);
167
175
  const selected = await ctx.ui.select(title, labels);
168
176
  const index = labels.indexOf(selected ?? "");
169
177
  return index < 0 ? undefined : items[index]!.value;
@@ -244,7 +252,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
244
252
  if (!ctx.hasUI) {
245
253
  const inventory = await adapter.isolated(ctx.cwd, current);
246
254
  const report = fitItems("Isolated requests (report only):", inventory.requests,
247
- (request) => clean(`${request.orphaned ? "orphaned · " : ""}${request.name} · ${request.status} · ${request.id}`, 500), ROLES_REPORT_CODE_POINTS,
255
+ (request) => clean(`${request.orphaned ? "orphaned · " : ""}${isolatedLabel(request)}`, 500), ROLES_REPORT_CODE_POINTS,
248
256
  "open /subagent with interactive UI to inspect them.");
249
257
  pi.sendMessage({ customType: "pi-subagent-discard", content: `${report.text}\nUnreadable state files preserved: ${inventory.invalidIds.length}.\nRemoval requires interactive confirmation.`, display: true });
250
258
  return;
@@ -262,19 +270,21 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
262
270
  if (unavailable) ctx.ui.notify(`Isolated inventory unavailable (${unavailable}). Direct branch recovery remains available; check the canonical Git checkout/configuration.`, "warning");
263
271
  if (inventory?.invalidIds.length) for (const id of inventory.invalidIds) ctx.ui.notify(`Unreadable isolated state ID: ${JSON.stringify(id)}. Preserve this file; inspect the state store before making changes.`, "warning");
264
272
  type Selection = { kind: "direct"; task: DirectTask } | { kind: "isolated"; request: IsolatedRequest; root: string } | { kind: "refresh" } | { kind: "history" } | { kind: "close" };
273
+ // A header row selects Refresh; rows sort by status priority within their group.
274
+ const group = <T extends { status: string }>(title: string, items: readonly T[], choice: (item: T) => Choice<Selection>): Choice<Selection>[] =>
275
+ items.length ? [{ label: `── ${title} (${items.length}) ──`, value: { kind: "refresh" } },
276
+ ...[...items].sort((a, b) => priority(a.status) - priority(b.status)).map(choice)] : [];
277
+ const requests = inventory?.requests ?? [];
278
+ const isolatedChoice = (request: IsolatedRequest): Choice<Selection> => ({ label: ` ${isolatedLabel(request)}`, value: { kind: "isolated", request, root: inventory!.root } });
265
279
  const selections: Choice<Selection>[] = [
266
280
  // One row per workflow; new records keep opaque IDs in details. Status is last so clipping preserves the reference.
267
- ...direct.filter((task) => history || !completed(task.status)).map((task) => ({ label: `Direct · ${directLabel(task)} · ${task.status}${costSuffix(task.cost)}${task.workers.length || task.name === task.id ? "" : ` · ${task.id}`}`, value: { kind: "direct" as const, task } })),
268
- ...(inventory?.requests ?? []).filter((request) => request.orphaned || history || !completed(request.status) || /retained/i.test(request.status)).map((request) => ({ label: `Isolated · ${request.orphaned ? "orphaned · " : ""}${request.name} · ${request.status} · ${request.id}`, value: { kind: "isolated" as const, request, root: inventory!.root } })),
269
- { label: "Refresh", value: { kind: "refresh" as const } },
270
- { label: history ? "Active work" : "Completed / history (including retained work)", value: { kind: "history" as const } },
271
- { label: "Close", value: { kind: "close" as const } },
281
+ ...group("Direct", direct.filter((task) => history || !completed(task.status)), (task) => ({ label: ` ${directLabel(task)} · ${task.status}${costSuffix(task.cost)}${task.workers.length || task.name === task.id ? "" : ` · ${task.id}`}`, value: { kind: "direct", task } })),
282
+ ...group("Isolated", requests.filter((request) => !request.orphaned && (history || !completed(request.status) || /retained/i.test(request.status))), isolatedChoice),
283
+ ...group("Orphaned · parent checkout removed", requests.filter((request) => request.orphaned), isolatedChoice),
284
+ { label: "Refresh", value: { kind: "refresh" } },
285
+ { label: history ? "Active work" : "Completed / history (including retained work)", value: { kind: "history" } },
286
+ { label: "Close", value: { kind: "close" } },
272
287
  ];
273
- const taskChoices = selections.splice(0, selections.length - 3);
274
- const rank = ({ value }: Choice<Selection>) => value.kind === "direct" ? priority(value.task.status)
275
- : value.kind === "isolated" ? priority(value.request.status) : 3;
276
- taskChoices.sort((a, b) => rank(a) - rank(b));
277
- selections.unshift(...taskChoices);
278
288
  if (!unavailable && !direct.length && !inventory?.requests.length && !inventory?.invalidIds.length) ctx.ui.notify("No direct tasks recorded on this session branch or isolated requests in this checkout. Ask Main to start delegation.", "info");
279
289
  const selected = await choose(ctx, history ? "Subagent · completed / history" : "Subagent · current work", selections);
280
290
  if (!valid() || !selected || selected.kind === "close") return;
@@ -325,7 +335,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
325
335
  ...(eligible.length ? [{ label: "Send follow-up instructions", value: "send" as const }, { label: "Edit queued instructions (withdraw ALL pending; Cancel leaves withdrawn)", value: "edit" as const }] : []),
326
336
  { label: "Back / refresh", value: "back" },
327
337
  ];
328
- const action = await choose(ctx, `Isolated · ${clean(request.name)} · ${request.id}`, actions);
338
+ const action = await choose(ctx, `${request.orphaned ? "Orphaned" : "Isolated"} · ${request.id} · ${summary(request.name)}`, actions);
329
339
  if (!valid() || !action) return;
330
340
  if (action === "back") break;
331
341
  if (action === "discard") {
@@ -356,7 +366,7 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
356
366
  }
357
367
  const live = request.tasks.filter((task) => task.kind === "changeset" && adapter.canFollowup(root, request.id, task.id));
358
368
  if (!live.length) { ctx.ui.notify("No locally active unsealed changeset remains. Refresh to see current status.", "warning"); continue; }
359
- const task = await choose(ctx, `Select task for ${action === "edit" ? "withdrawal and edit" : "follow-up"}`, live.map((item) => ({ label: `${item.name} · ${item.status} · ${item.id}`, value: item })));
369
+ const task = await choose(ctx, `Select task for ${action === "edit" ? "withdrawal and edit" : "follow-up"}`, live.map((item) => ({ label: `${item.id} · ${item.status} · ${summary(item.name)}`, value: item })));
360
370
  if (!valid() || !task) return;
361
371
  let prefill = "";
362
372
  if (action === "edit") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "27.6.0",
3
+ "version": "27.6.1",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",