@gajae-code/coding-agent 0.12.1 → 0.12.4

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
  7. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  8. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  10. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  11. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  12. package/dist/types/modes/interactive-mode.d.ts +9 -0
  13. package/dist/types/modes/theme/theme.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +32 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  16. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  19. package/dist/types/task/index.d.ts +4 -0
  20. package/dist/types/task/output-manager.d.ts +1 -0
  21. package/dist/types/task/receipt.d.ts +2 -0
  22. package/dist/types/tools/index.d.ts +2 -0
  23. package/package.json +8 -8
  24. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  25. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  26. package/scripts/run-acp-conformance.ts +5 -0
  27. package/src/async/job-manager.ts +22 -20
  28. package/src/config/provider-ranking.ts +1 -0
  29. package/src/config/settings-schema.ts +2 -2
  30. package/src/debug/index.ts +42 -17
  31. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  33. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  34. package/src/gjc-runtime/launch-worktree.ts +103 -1
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  37. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  38. package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
  39. package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
  40. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  41. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  42. package/src/internal-urls/docs-index.generated.ts +2 -2
  43. package/src/modes/DESIGN.md +53 -0
  44. package/src/modes/components/irc-sidebar.ts +30 -1
  45. package/src/modes/components/tool-status-header.ts +0 -1
  46. package/src/modes/controllers/command-controller.ts +38 -34
  47. package/src/modes/controllers/event-controller.ts +27 -16
  48. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  49. package/src/modes/controllers/selector-controller.ts +115 -22
  50. package/src/modes/interactive-mode.ts +189 -62
  51. package/src/modes/jobs-observer.ts +1 -1
  52. package/src/modes/theme/theme.ts +11 -9
  53. package/src/modes/types.ts +88 -0
  54. package/src/sdk/broker/lifecycle.ts +18 -13
  55. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  56. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  57. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  58. package/src/sdk/session.ts +1 -0
  59. package/src/session/agent-session.ts +22 -1
  60. package/src/session/internal/managed-session-scope.ts +158 -107
  61. package/src/session/internal/managed-session-storage.ts +445 -57
  62. package/src/session/session-storage.ts +29 -44
  63. package/src/task/index.ts +257 -31
  64. package/src/task/output-manager.ts +24 -14
  65. package/src/task/receipt.ts +11 -1
  66. package/src/tools/index.ts +2 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Product-surface dogfood for #2902 ralplan typed review conflicts.
3
+ *
4
+ * Uses the **compiled** `packages/coding-agent/dist/gjc` binary (not source
5
+ * `cli.ts`) so evidence matches the owner exact-head compiled-binary gate:
6
+ * 1) open conflicts fail closed (exit 2, Join blocked)
7
+ * 2) complete disposition document is accepted and persisted
8
+ * 3) stored artifact is dispositioned under ralplan.review_conflicts.v1
9
+ * 4) spoofed receipts fail closed against the run index
10
+ *
11
+ * Prerequisites (from monorepo root, same HEAD):
12
+ * bun run build
13
+ * bun packages/coding-agent/scripts/dogfood-ralplan-review-conflicts.ts
14
+ *
15
+ * Optional: GJC_BINARY=/path/to/gjc overrides the default dist path.
16
+ */
17
+ import * as fsp from "node:fs/promises";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
23
+ const defaultBinary = path.join(repoRoot, "packages/coding-agent/dist/gjc");
24
+
25
+ async function resolveCompiledBinary(): Promise<string> {
26
+ const binary = process.env.GJC_BINARY?.trim() || defaultBinary;
27
+ try {
28
+ const st = await fsp.stat(binary);
29
+ if (!st.isFile()) throw new Error(`not a file: ${binary}`);
30
+ // Ensure executable bit is present for direct spawn.
31
+ await fsp.access(binary, fsp.constants.X_OK).catch(async () => {
32
+ await fsp.chmod(binary, 0o755);
33
+ });
34
+ } catch (error) {
35
+ throw new Error(
36
+ `Compiled binary missing or not executable: ${binary}. Run \`bun run build\` on this exact HEAD first. (${error instanceof Error ? error.message : String(error)})`,
37
+ );
38
+ }
39
+ return binary;
40
+ }
41
+
42
+ async function runGjc(
43
+ binary: string,
44
+ cwd: string,
45
+ args: string[],
46
+ env: NodeJS.ProcessEnv,
47
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
48
+ const proc = Bun.spawn([binary, ...args], { cwd, env, stdout: "pipe", stderr: "pipe" });
49
+ const [code, stdout, stderr] = await Promise.all([
50
+ proc.exited,
51
+ new Response(proc.stdout).text(),
52
+ new Response(proc.stderr).text(),
53
+ ]);
54
+ return { code, stdout, stderr };
55
+ }
56
+
57
+ async function main(): Promise<void> {
58
+ const binary = await resolveCompiledBinary();
59
+ const dogfoodRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "gjc-dogfood-2902-"));
60
+ const sessionId = `dogfood-2902-${process.pid}`;
61
+ const env = { ...process.env, GJC_SESSION_ID: sessionId };
62
+ const full = Bun.spawnSync(["git", "-C", repoRoot, "rev-parse", "HEAD"]).stdout.toString().trim();
63
+ const short = Bun.spawnSync(["git", "-C", repoRoot, "rev-parse", "--short=8", "HEAD"]).stdout.toString().trim();
64
+ const binaryStat = await fsp.stat(binary);
65
+
66
+ console.log("# Dogfood: ralplan review conflicts (#2902) — compiled binary");
67
+ console.log(`root=${dogfoodRoot}`);
68
+ console.log(`session=${sessionId}`);
69
+ console.log(`binary=${binary}`);
70
+ console.log(`binary_size=${binaryStat.size}`);
71
+ console.log(`binary_mtime=${binaryStat.mtime.toISOString()}`);
72
+ console.log(`commit=${short}`);
73
+ console.log(`commit_full=${full}`);
74
+ console.log(`bun=${Bun.version}`);
75
+ console.log();
76
+
77
+ // Seed ralplan run state so --write has an active run.
78
+ console.log("## 1) compiled gjc ralplan seed");
79
+ const seed = await runGjc(binary, dogfoodRoot, ["ralplan", "--deliberate", "--json", "dogfood #2902"], env);
80
+ console.log(`exit=${seed.code}`);
81
+ console.log((seed.stdout || seed.stderr).trim());
82
+ if (seed.code !== 0) process.exit(1);
83
+
84
+ // Persist same-pass Architect/Critic artifacts so disposition receipts resolve.
85
+ console.log();
86
+ console.log("## 2) seed architect + critic stage artifacts (same-pass stage_n=1)");
87
+ const architect = await runGjc(
88
+ binary,
89
+ dogfoodRoot,
90
+ ["ralplan", "--write", "--stage", "architect", "--stage_n", "1", "--artifact", "# architect", "--json"],
91
+ env,
92
+ );
93
+ console.log(`architect exit=${architect.code}`);
94
+ if (architect.code !== 0) {
95
+ console.error(architect.stderr || architect.stdout);
96
+ process.exit(1);
97
+ }
98
+ const critic = await runGjc(
99
+ binary,
100
+ dogfoodRoot,
101
+ ["ralplan", "--write", "--stage", "critic", "--stage_n", "1", "--artifact", "# critic", "--json"],
102
+ env,
103
+ );
104
+ console.log(`critic exit=${critic.code}`);
105
+ if (critic.code !== 0) {
106
+ console.error(critic.stderr || critic.stdout);
107
+ process.exit(1);
108
+ }
109
+ const archReceipt = JSON.parse(architect.stdout) as { path: string; sha256: string };
110
+ const critReceipt = JSON.parse(critic.stdout) as { path: string; sha256: string };
111
+ console.log(`architect path=${archReceipt.path}`);
112
+ console.log(`critic path=${critReceipt.path}`);
113
+
114
+ const findings = [
115
+ {
116
+ findingId: "arch-1",
117
+ targetId: "contract.field",
118
+ action: "remove",
119
+ severity: "block",
120
+ evidence: "redundant with session identity",
121
+ sourceRole: "architect",
122
+ sourceReceipt: {
123
+ stage: "architect",
124
+ stageN: 1,
125
+ path: archReceipt.path,
126
+ sha256: archReceipt.sha256,
127
+ },
128
+ },
129
+ {
130
+ findingId: "crit-1",
131
+ targetId: "contract.field",
132
+ action: "add",
133
+ severity: "watch",
134
+ evidence: "needed for multi-repo binding",
135
+ sourceRole: "critic",
136
+ sourceReceipt: {
137
+ stage: "critic",
138
+ stageN: 1,
139
+ path: critReceipt.path,
140
+ sha256: critReceipt.sha256,
141
+ },
142
+ },
143
+ ];
144
+
145
+ // 3) Open disposition must fail closed.
146
+ const openPath = path.join(dogfoodRoot, "open-disposition.json");
147
+ await fsp.writeFile(
148
+ openPath,
149
+ JSON.stringify({
150
+ schema: "ralplan.review_conflicts.v1",
151
+ plannerStageN: 1,
152
+ findings,
153
+ dispositions: [],
154
+ }),
155
+ );
156
+ console.log();
157
+ console.log("## 3) disposition stage with open conflicts (expect fail-closed)");
158
+ const open = await runGjc(
159
+ binary,
160
+ dogfoodRoot,
161
+ ["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", openPath],
162
+ env,
163
+ );
164
+ console.log(`exit=${open.code}`);
165
+ console.log((open.stderr || open.stdout).trim());
166
+ if (open.code !== 2 || !`${open.stderr}${open.stdout}`.includes("Join blocked")) {
167
+ console.error("FAIL: expected exit=2 and Join blocked for open conflicts");
168
+ process.exit(1);
169
+ }
170
+ console.log("fail_closed: ok");
171
+
172
+ // 4) Spoofed receipt must fail closed.
173
+ const spoofPath = path.join(dogfoodRoot, "spoof-disposition.json");
174
+ await fsp.writeFile(
175
+ spoofPath,
176
+ JSON.stringify({
177
+ schema: "ralplan.review_conflicts.v1",
178
+ plannerStageN: 1,
179
+ findings: [
180
+ {
181
+ ...findings[0],
182
+ sourceReceipt: {
183
+ stage: "architect",
184
+ stageN: 1,
185
+ path: "/tmp/spoofed.md",
186
+ sha256: "deadbeef",
187
+ },
188
+ },
189
+ findings[1],
190
+ ],
191
+ dispositions: [
192
+ {
193
+ conflictId: "conflict:contract.field:arch-1:crit-1",
194
+ choice: "accept_architect",
195
+ rationale: "spoof should not land",
196
+ decisionOwner: "ralplan-leader",
197
+ affectedSections: ["## Contracts"],
198
+ },
199
+ ],
200
+ }),
201
+ );
202
+ console.log();
203
+ console.log("## 4) disposition with spoofed receipt (expect fail-closed)");
204
+ const spoof = await runGjc(
205
+ binary,
206
+ dogfoodRoot,
207
+ ["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", spoofPath],
208
+ env,
209
+ );
210
+ console.log(`exit=${spoof.code}`);
211
+ console.log((spoof.stderr || spoof.stdout).trim());
212
+ if (spoof.code !== 2 || !`${spoof.stderr}${spoof.stdout}`.includes("does not match indexed")) {
213
+ console.error("FAIL: expected exit=2 for spoofed receipt");
214
+ process.exit(1);
215
+ }
216
+ console.log("spoof_rejected: ok");
217
+
218
+ // 5) Complete disposition must accept and persist.
219
+ const closedPath = path.join(dogfoodRoot, "closed-disposition.json");
220
+ await fsp.writeFile(
221
+ closedPath,
222
+ JSON.stringify({
223
+ schema: "ralplan.review_conflicts.v1",
224
+ plannerStageN: 1,
225
+ findings,
226
+ dispositions: [
227
+ {
228
+ conflictId: "conflict:contract.field:arch-1:crit-1",
229
+ choice: "accept_architect",
230
+ rationale: "Field duplicates existing session identity.",
231
+ decisionOwner: "ralplan-leader",
232
+ affectedSections: ["## Contracts"],
233
+ },
234
+ ],
235
+ }),
236
+ );
237
+ console.log();
238
+ console.log("## 5) disposition stage with complete dispositions (expect accept)");
239
+ const closed = await runGjc(
240
+ binary,
241
+ dogfoodRoot,
242
+ ["ralplan", "--write", "--stage", "disposition", "--stage_n", "1", "--artifact", closedPath, "--json"],
243
+ env,
244
+ );
245
+ console.log(`exit=${closed.code}`);
246
+ console.log(closed.stdout.trim() || closed.stderr.trim());
247
+ if (closed.code !== 0) {
248
+ console.error("FAIL: expected disposition write success");
249
+ process.exit(1);
250
+ }
251
+ const payload = JSON.parse(closed.stdout) as { path?: string; stage?: string };
252
+ if (payload.stage !== "disposition" || !payload.path) {
253
+ console.error("FAIL: unexpected write payload", payload);
254
+ process.exit(1);
255
+ }
256
+ const body = await fsp.readFile(payload.path, "utf-8");
257
+ console.log();
258
+ console.log("## 6) persisted disposition artifact");
259
+ console.log(`path=${payload.path}`);
260
+ console.log(body.slice(0, 1200));
261
+ if (!body.includes("ralplan.review_conflicts.v1") || !body.includes("dispositioned")) {
262
+ console.error("FAIL: artifact missing schema or dispositioned status");
263
+ process.exit(1);
264
+ }
265
+
266
+ console.log();
267
+ console.log("DOGFOOD_OK");
268
+ console.log(`DOGFOOD_BINARY=${binary}`);
269
+ console.log(`DOGFOOD_HEAD=${full}`);
270
+ }
271
+
272
+ await main();
@@ -35,6 +35,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
35
35
  "slash_command:transcript": "visual/local-only transcript viewer, not a user-facing SDK control seam",
36
36
  "slash_command:sessions": "visual/local-only sessions dashboard, not a user-facing SDK control seam",
37
37
  "agent_session:constructor": "internal accessor/plumbing, not a user-facing control seam",
38
+ "agent_session:registerToolSessionCleanup":
39
+ "internal tool lifecycle cleanup registration, not a user-facing SDK control seam",
38
40
  "agent_session:nextToolChoice": "internal accessor/plumbing, not a user-facing control seam",
39
41
  "agent_session:setForcedToolChoice": "internal accessor/plumbing, not a user-facing control seam",
40
42
  "agent_session:getActiveSkillState": "internal accessor/plumbing, not a user-facing control seam",
@@ -225,6 +225,11 @@ async function checkoutAcpxSource(): Promise<AcpxCheckout> {
225
225
  await commandOutput(["git", "clean", "-fdx", "--exclude=node_modules"], root);
226
226
  const status = await commandOutput(["git", "status", "--porcelain"], root);
227
227
  if (status.length > 0) throw new Error(`acpx source checkout is not clean at ${ACPX_GIT_HEAD}.`);
228
+ // The upstream runner resolves its own imports (`@agentclientprotocol/sdk`, `zod`)
229
+ // from this checkout, not from the gjc workspace, so the pin has to be installed
230
+ // before it can run. `git clean` deliberately preserves node_modules so a warm
231
+ // cache skips the reinstall.
232
+ await commandOutput(["bun", "install", "--no-save", "--ignore-scripts"], root);
228
233
  return { root, head: await commandOutput(["git", "rev-parse", "HEAD"], root) };
229
234
  }
230
235
 
@@ -525,9 +525,9 @@ export class AsyncJobManager {
525
525
 
526
526
  if (job.status === "cancelled") {
527
527
  job.resultText = outcome.kind === "paused" ? outcome.note : outcome.text;
528
+ this.#markRecordTerminal(id, "cancelled");
528
529
  this.#runLifecycle(id, "terminal", job);
529
530
  this.#scheduleEviction(id);
530
- this.#markRecordTerminal(id, "cancelled");
531
531
  this.#drainResumeQueue();
532
532
  return;
533
533
  }
@@ -539,6 +539,7 @@ export class AsyncJobManager {
539
539
  this.#freezeEndTime(job);
540
540
  if (outcome.note) job.resultText = outcome.note;
541
541
  this.#markRecordPaused(id);
542
+ this.#notifyChange();
542
543
  this.#drainResumeQueue();
543
544
  return;
544
545
  }
@@ -547,10 +548,10 @@ export class AsyncJobManager {
547
548
  job.setupFailureSummary = outcome.setupFailureSummary;
548
549
  this.#freezeEndTime(job);
549
550
  job.errorText = outcome.text;
551
+ this.#markRecordTerminal(id, "failed");
550
552
  this.#enqueueDelivery(id, outcome.text);
551
553
  this.#runLifecycle(id, "terminal", job);
552
554
  this.#scheduleEviction(id);
553
- this.#markRecordTerminal(id, "failed");
554
555
  this.#drainResumeQueue();
555
556
  return;
556
557
  }
@@ -558,28 +559,28 @@ export class AsyncJobManager {
558
559
  job.status = "completed";
559
560
  this.#freezeEndTime(job);
560
561
  job.resultText = outcome.text;
562
+ this.#markRecordTerminal(id, "completed");
561
563
  this.#enqueueDelivery(id, outcome.text);
562
564
  this.#runLifecycle(id, "terminal", job);
563
565
  this.#scheduleEviction(id);
564
- this.#markRecordTerminal(id, "completed");
565
566
  this.#drainResumeQueue();
566
567
  } catch (error) {
567
568
  if (job.status === "cancelled") {
568
569
  job.errorText = error instanceof Error ? error.message : String(error);
570
+ this.#markRecordTerminal(id, "cancelled");
569
571
  this.#runLifecycle(id, "terminal", job);
570
572
  this.#scheduleEviction(id);
571
- this.#markRecordTerminal(id, "cancelled");
572
573
  this.#drainResumeQueue();
573
574
  return;
574
575
  }
575
- this.#runLifecycle(id, "terminal", job);
576
576
  const errorText = error instanceof Error ? error.message : String(error);
577
577
  job.status = "failed";
578
578
  this.#freezeEndTime(job);
579
579
  job.errorText = errorText;
580
+ this.#markRecordTerminal(id, "failed");
580
581
  this.#enqueueDelivery(id, errorText);
582
+ this.#runLifecycle(id, "terminal", job);
581
583
  this.#scheduleEviction(id);
582
- this.#markRecordTerminal(id, "failed");
583
584
  this.#drainResumeQueue();
584
585
  }
585
586
  })();
@@ -599,20 +600,22 @@ export class AsyncJobManager {
599
600
  if (!job) return false;
600
601
  if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
601
602
  if (job.status === "paused") {
602
- this.#runLifecycle(id, "cancel");
603
603
  // Paused jobs have no running promise to abort; transition directly.
604
604
  // The session file is kept, so the record stays resumable by id.
605
605
  job.status = "cancelled";
606
606
  this.#markRecordTerminal(id, "cancelled");
607
+ this.#runLifecycle(id, "cancel");
607
608
  this.#scheduleEviction(id);
608
609
  this.#drainResumeQueue();
609
610
  return true;
610
611
  }
611
612
  if (job.status !== "running") return false;
612
- this.#runLifecycle(id, "cancel");
613
613
  job.status = "cancelled";
614
614
  this.#freezeEndTime(job);
615
+ this.#markRecordTerminal(id, "cancelled");
616
+ this.#runLifecycle(id, "cancel");
615
617
  job.abortController.abort();
618
+ this.#notifyChange();
616
619
  return true;
617
620
  }
618
621
 
@@ -696,6 +699,7 @@ export class AsyncJobManager {
696
699
  /** Register or replace the canonical record for a subagent. */
697
700
  registerSubagentRecord(record: SubagentRecord): void {
698
701
  this.#subagentRecords.set(record.subagentId, record);
702
+ this.#notifyChange();
699
703
  }
700
704
 
701
705
  /** Patch model metadata onto an existing subagent record (best-effort; no-op if unknown). */
@@ -1136,6 +1140,7 @@ export class AsyncJobManager {
1136
1140
  message,
1137
1141
  createdAt: rec.queued.createdAt,
1138
1142
  });
1143
+ this.#notifyChange();
1139
1144
  return { ok: true, queued: true, status: "queued" };
1140
1145
  }
1141
1146
  return this.#startResume(rec, message, descriptor);
@@ -1160,6 +1165,7 @@ export class AsyncJobManager {
1160
1165
  rec.currentJobId = newJobId;
1161
1166
  rec.status = this.#jobs.get(newJobId)?.status ?? "running";
1162
1167
  rec.queued = undefined;
1168
+ this.#notifyChange();
1163
1169
  return { ok: true, status: rec.status, jobId: newJobId };
1164
1170
  }
1165
1171
 
@@ -1202,16 +1208,15 @@ export class AsyncJobManager {
1202
1208
  if (!rec) return false;
1203
1209
  if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
1204
1210
  if (rec.status === "paused") {
1205
- if (rec.currentJobId) {
1206
- const job = this.#jobs.get(rec.currentJobId);
1207
- if (job && job.status === "paused") {
1208
- job.status = "cancelled";
1209
- this.#scheduleEviction(rec.currentJobId);
1210
- }
1211
- }
1211
+ const currentJobId = rec.currentJobId;
1212
+ const job = currentJobId ? this.#jobs.get(currentJobId) : undefined;
1213
+ const shouldScheduleEviction = job?.status === "paused";
1214
+ if (shouldScheduleEviction) job.status = "cancelled";
1212
1215
  rec.status = "cancelled";
1213
1216
  this.#liveHandles.delete(rec.subagentId);
1214
1217
  this.#subagentProgress.delete(rec.subagentId);
1218
+ if (shouldScheduleEviction && currentJobId) this.#scheduleEviction(currentJobId);
1219
+ else this.#notifyChange();
1215
1220
  this.#drainResumeQueue();
1216
1221
  return true;
1217
1222
  }
@@ -1221,6 +1226,7 @@ export class AsyncJobManager {
1221
1226
  rec.status = "cancelled";
1222
1227
  rec.queued = undefined;
1223
1228
  this.#subagentProgress.delete(rec.subagentId);
1229
+ this.#notifyChange();
1224
1230
  return true;
1225
1231
  }
1226
1232
  return false;
@@ -1471,11 +1477,7 @@ export class AsyncJobManager {
1471
1477
  */
1472
1478
  cancelAll(filter?: AsyncJobFilter): void {
1473
1479
  for (const job of this.getRunningJobs(filter)) {
1474
- this.#runLifecycle(job.id, "cancel");
1475
- job.status = "cancelled";
1476
- this.#freezeEndTime(job);
1477
- job.abortController.abort();
1478
- this.#scheduleEviction(job.id);
1480
+ if (this.cancel(job.id, filter)) this.#scheduleEviction(job.id);
1479
1481
  }
1480
1482
  }
1481
1483
 
@@ -53,6 +53,7 @@ export const FAMOUS_PROVIDER_ORDER: readonly string[] = [
53
53
  "xiaomi-token-plan-cn",
54
54
  "opengateway",
55
55
  "bizrouter",
56
+ "mara",
56
57
  "github-copilot",
57
58
  "cursor",
58
59
  ];
@@ -873,8 +873,8 @@ export const SETTINGS_SCHEMA = {
873
873
  default: true,
874
874
  ui: {
875
875
  tab: "appearance",
876
- label: "Status Line Action Hints",
877
- description: "Show contextual keyboard shortcuts in the status line",
876
+ label: "Composer Shortcut Hints",
877
+ description: "Show contextual keyboard shortcuts in the composer placeholder",
878
878
  },
879
879
  },
880
880
 
@@ -11,6 +11,7 @@ import { getSessionsDir } from "@gajae-code/utils";
11
11
  import { DynamicBorder } from "../modes/components/dynamic-border";
12
12
  import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
13
13
  import type { InteractiveModeContext } from "../modes/types";
14
+ import { suspendInteractiveActivityIndicator } from "../modes/types";
14
15
  import { formatBytes } from "../tools/render-utils";
15
16
  import { openPath } from "../utils/open";
16
17
  import { DebugLogViewerComponent } from "./log-viewer";
@@ -77,10 +78,12 @@ export class DebugSelectorComponent extends Container {
77
78
  }
78
79
 
79
80
  handleInput(keyData: string): void {
81
+ if (this.ctx.isStopped?.()) return;
80
82
  this.#selectList.handleInput(keyData);
81
83
  }
82
84
 
83
85
  async #handleSelection(value: string): Promise<void> {
86
+ if (this.ctx.isStopped?.()) return;
84
87
  switch (value) {
85
88
  case "open-artifacts":
86
89
  await this.#handleOpenArtifacts();
@@ -115,12 +118,23 @@ export class DebugSelectorComponent extends Container {
115
118
  }
116
119
  }
117
120
 
121
+ #finishStatusLoader(loader: Loader, releaseActivityIndicator: () => void): void {
122
+ loader.stop();
123
+ if (!this.ctx.isStopped?.()) this.ctx.statusContainer.clear();
124
+ releaseActivityIndicator();
125
+ }
126
+
118
127
  async #handlePerformanceReport(): Promise<void> {
119
128
  // Start profiling
120
129
  let session: ProfilerSession;
121
130
  try {
122
131
  session = await startCpuProfile();
132
+ if (this.ctx.isStopped?.()) {
133
+ await session.stop().catch(() => {});
134
+ return;
135
+ }
123
136
  } catch (err) {
137
+ if (this.ctx.isStopped?.()) return;
124
138
  this.ctx.showError(`Failed to start profiler: ${err instanceof Error ? err.message : String(err)}`);
125
139
  return;
126
140
  }
@@ -151,9 +165,17 @@ export class DebugSelectorComponent extends Container {
151
165
  resolve();
152
166
  };
153
167
 
154
- await promise;
168
+ const stopped = Promise.withResolvers<void>();
169
+ const unsubscribeStop = this.ctx.onStop?.(() => stopped.resolve());
170
+ const stopWon = await Promise.race([promise.then(() => false), stopped.promise.then(() => true)]);
171
+ unsubscribeStop?.();
172
+ if (stopWon || this.ctx.isStopped?.()) {
173
+ await session.stop().catch(() => {});
174
+ return;
175
+ }
155
176
 
156
177
  // Stop profiling and create report
178
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
157
179
  const loader = new Loader(
158
180
  this.ctx.ui,
159
181
  spinner => theme.fg("accent", spinner),
@@ -174,8 +196,8 @@ export class DebugSelectorComponent extends Container {
174
196
  workProfile,
175
197
  });
176
198
 
177
- loader.stop();
178
- this.ctx.statusContainer.clear();
199
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
200
+ if (this.ctx.isStopped?.()) return;
179
201
 
180
202
  this.ctx.chatContainer.addChild(new Spacer(1));
181
203
  this.ctx.chatContainer.addChild(
@@ -184,8 +206,8 @@ export class DebugSelectorComponent extends Container {
184
206
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
185
207
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
186
208
  } catch (err) {
187
- loader.stop();
188
- this.ctx.statusContainer.clear();
209
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
210
+ if (this.ctx.isStopped?.()) return;
189
211
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
190
212
  }
191
213
 
@@ -219,6 +241,7 @@ export class DebugSelectorComponent extends Container {
219
241
  }
220
242
 
221
243
  async #handleDumpReport(): Promise<void> {
244
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
222
245
  const loader = new Loader(
223
246
  this.ctx.ui,
224
247
  spinner => theme.fg("accent", spinner),
@@ -235,8 +258,8 @@ export class DebugSelectorComponent extends Container {
235
258
  settings: this.#getResolvedSettings(),
236
259
  });
237
260
 
238
- loader.stop();
239
- this.ctx.statusContainer.clear();
261
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
262
+ if (this.ctx.isStopped?.()) return;
240
263
 
241
264
  this.ctx.chatContainer.addChild(new Spacer(1));
242
265
  this.ctx.chatContainer.addChild(
@@ -245,8 +268,8 @@ export class DebugSelectorComponent extends Container {
245
268
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
246
269
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
247
270
  } catch (err) {
248
- loader.stop();
249
- this.ctx.statusContainer.clear();
271
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
272
+ if (this.ctx.isStopped?.()) return;
250
273
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
251
274
  }
252
275
 
@@ -254,6 +277,7 @@ export class DebugSelectorComponent extends Container {
254
277
  }
255
278
 
256
279
  async #handleMemoryReport(): Promise<void> {
280
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
257
281
  const loader = new Loader(
258
282
  this.ctx.ui,
259
283
  spinner => theme.fg("accent", spinner),
@@ -274,8 +298,8 @@ export class DebugSelectorComponent extends Container {
274
298
  heapSnapshot,
275
299
  });
276
300
 
277
- loader.stop();
278
- this.ctx.statusContainer.clear();
301
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
302
+ if (this.ctx.isStopped?.()) return;
279
303
 
280
304
  this.ctx.chatContainer.addChild(new Spacer(1));
281
305
  this.ctx.chatContainer.addChild(
@@ -284,8 +308,8 @@ export class DebugSelectorComponent extends Container {
284
308
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
285
309
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
286
310
  } catch (err) {
287
- loader.stop();
288
- this.ctx.statusContainer.clear();
311
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
312
+ if (this.ctx.isStopped?.()) return;
289
313
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
290
314
  }
291
315
 
@@ -405,6 +429,7 @@ export class DebugSelectorComponent extends Container {
405
429
  }
406
430
 
407
431
  // Clear cache
432
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
408
433
  const loader = new Loader(
409
434
  this.ctx.ui,
410
435
  spinner => theme.fg("accent", spinner),
@@ -418,8 +443,8 @@ export class DebugSelectorComponent extends Container {
418
443
  try {
419
444
  const result = await clearArtifactCache(sessionsDir, 30);
420
445
 
421
- loader.stop();
422
- this.ctx.statusContainer.clear();
446
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
447
+ if (this.ctx.isStopped?.()) return;
423
448
 
424
449
  this.ctx.chatContainer.addChild(new Spacer(1));
425
450
  this.ctx.chatContainer.addChild(
@@ -430,8 +455,8 @@ export class DebugSelectorComponent extends Container {
430
455
  ),
431
456
  );
432
457
  } catch (err) {
433
- loader.stop();
434
- this.ctx.statusContainer.clear();
458
+ this.#finishStatusLoader(loader, releaseActivityIndicator);
459
+ if (this.ctx.isStopped?.()) return;
435
460
  this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
436
461
  }
437
462