@arhen/pi-core-subagent 1.1.14 → 1.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1027,6 +1027,22 @@ class SubagentManager {
1027
1027
  return { run: cloneRun(run), background: true };
1028
1028
  }
1029
1029
 
1030
+ /** Abort ONE task; siblings keep running. Returns false when unknown or already finished. */
1031
+ cancelTask(runId: string, taskId: string, ctx?: ExtensionContext): boolean {
1032
+ const run = this.runs.get(runId);
1033
+ const task = run?.tasks.find((t) => t.id === taskId);
1034
+ if (!run || !task || TERMINAL.includes(task.status)) return false;
1035
+ // Mark first: runChild's catch reads task.status to classify the outcome as aborted.
1036
+ task.status = "aborted";
1037
+ task.error = task.error || "Canceled from peek";
1038
+ task.endedAt = Date.now();
1039
+ this.liveChildren.get(`${runId}:${taskId}`)?.abort();
1040
+ this.mailboxes.close(`${runId}:${taskId}`);
1041
+ if (ctx) this.flushWidget(run, ctx);
1042
+ this.emit("subagent:task-aborted", { runId, taskId });
1043
+ return true;
1044
+ }
1045
+
1030
1046
  cancelRun(runId: string): { aborted: number } {
1031
1047
  const run = this.runs.get(runId);
1032
1048
  if (!run) return { aborted: 0 };
@@ -1178,13 +1194,24 @@ export default function (pi: ExtensionAPI) {
1178
1194
  manager
1179
1195
  .listRuns()
1180
1196
  .flatMap((run) => run.tasks)
1181
- .map((task) => ({ agent: task.agent, status: task.status, sessionFile: task.sessionFile, line: taskLine(task) }));
1197
+ .map((task) => ({
1198
+ runId: task.runId,
1199
+ taskId: task.id,
1200
+ agent: task.agent,
1201
+ status: task.status,
1202
+ running: !TERMINAL.includes(task.status),
1203
+ sessionFile: task.sessionFile,
1204
+ line: taskLine(task),
1205
+ }));
1182
1206
  if (getTasks().length === 0) {
1183
1207
  ctx.ui.notify("No subagents in this session.", "info");
1184
1208
  return;
1185
1209
  }
1186
1210
  await ctx.ui.custom<void>(
1187
- (tui, theme, _keybindings, done) => createPeekPane(getTasks, theme, () => tui.requestRender(), () => done(undefined)),
1211
+ (tui, theme, _keybindings, done) =>
1212
+ createPeekPane(getTasks, theme, () => tui.requestRender(), () => done(undefined), (t) => {
1213
+ if (manager.cancelTask(t.runId, t.taskId, ctx)) ctx.ui.notify(`Aborted subagent ${t.agent}.`, "warning");
1214
+ }),
1188
1215
  { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "70%" } },
1189
1216
  );
1190
1217
  };
package/src/peek.ts CHANGED
@@ -15,8 +15,11 @@ const TAIL_BYTES = 64 * 1024;
15
15
  const POLL_MS = 700;
16
16
 
17
17
  export interface PeekTask {
18
+ runId: string;
19
+ taskId: string;
18
20
  agent: string;
19
21
  status: string;
22
+ running: boolean;
20
23
  sessionFile?: string;
21
24
  line: string; // pre-rendered stats line from the caller
22
25
  }
@@ -82,9 +85,16 @@ export interface PeekPane {
82
85
  * Build the peek component. `getTasks` is polled live, so the pane keeps
83
86
  * updating while agents run.
84
87
  */
85
- export function createPeekPane(getTasks: () => PeekTask[], theme: Theme, requestRender: () => void, close: () => void): PeekPane {
88
+ export function createPeekPane(
89
+ getTasks: () => PeekTask[],
90
+ theme: Theme,
91
+ requestRender: () => void,
92
+ close: () => void,
93
+ abort: (task: PeekTask) => void,
94
+ ): PeekPane {
86
95
  let selected = 0;
87
96
  let tailing = false;
97
+ let confirming = false;
88
98
  const timer = setInterval(requestRender, POLL_MS);
89
99
 
90
100
  const clamp = (n: number, len: number) => (len === 0 ? 0 : Math.max(0, Math.min(len - 1, n)));
@@ -95,7 +105,7 @@ export function createPeekPane(getTasks: () => PeekTask[], theme: Theme, request
95
105
  selected = clamp(selected, tasks.length);
96
106
  if (tasks.length === 0) return [theme.fg("dim", "No subagents in this session.")];
97
107
  const task = tasks[selected]!;
98
- const hint = tailing ? "esc back" : "↑↓ move · enter tail · esc close";
108
+ const hint = confirming ? theme.fg("error", `abort ${task.agent}? y / n`) : tailing ? "esc back · x abort" : "↑↓ move · enter tail · x abort · esc close";
99
109
  const head = `${theme.fg("accent", theme.bold(tailing ? task.agent : "Subagents"))} ${theme.fg("dim", `(${selected + 1}/${tasks.length}) · ${hint}`)}`;
100
110
  if (!tailing) {
101
111
  return [head, ...tasks.map((t, i) => truncateToWidth(`${i === selected ? theme.fg("accent", "❯ ") : " "}${t.line}`, width, "…"))];
@@ -105,8 +115,21 @@ export function createPeekPane(getTasks: () => PeekTask[], theme: Theme, request
105
115
  return [head, ...tailLines(task.sessionFile, 18).map((l) => truncateToWidth(` ${l}`, width, "…"))];
106
116
  },
107
117
  handleInput(data: string): void {
108
- const len = getTasks().length;
109
- if (matchesKey(data, Key.escape)) {
118
+ const tasks = getTasks();
119
+ const len = tasks.length;
120
+ if (confirming) {
121
+ // Abort is irreversible, so it always costs a second keystroke.
122
+ confirming = false;
123
+ if (data === "y" || data === "Y") {
124
+ const task = tasks[selected];
125
+ if (task) abort(task);
126
+ }
127
+ requestRender();
128
+ return;
129
+ }
130
+ if (data === "x" || data === "X") {
131
+ if (tasks[selected]?.running) confirming = true;
132
+ } else if (matchesKey(data, Key.escape)) {
110
133
  if (tailing) tailing = false;
111
134
  else close();
112
135
  } else if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {