@gajae-code/coding-agent 0.12.8 → 0.12.11

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  7. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  8. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  9. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
  10. package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
  11. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  12. package/dist/types/session/session-manager.d.ts +17 -0
  13. package/dist/types/session/session-storage.d.ts +1 -0
  14. package/dist/types/system-prompt.d.ts +12 -1
  15. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  16. package/dist/types/tools/computer.d.ts +35 -2
  17. package/dist/types/tools/todo-write.d.ts +2 -0
  18. package/package.json +8 -8
  19. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  20. package/src/cli/notify-cli.ts +12 -6
  21. package/src/commands/sdk.ts +54 -0
  22. package/src/config/model-profiles.ts +8 -0
  23. package/src/config/model-registry.ts +4 -1
  24. package/src/discovery/agents-md.ts +76 -19
  25. package/src/gjc-runtime/psmux-detect.ts +12 -16
  26. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  27. package/src/internal-urls/docs-index.generated.ts +6 -5
  28. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  29. package/src/modes/controllers/selector-controller.ts +61 -43
  30. package/src/modes/print-mode.ts +3 -0
  31. package/src/modes/types.ts +6 -0
  32. package/src/prompts/system/system-prompt.md +1 -0
  33. package/src/prompts/tools/computer.md +6 -1
  34. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  35. package/src/sdk/bus/daemon-paths.ts +2 -0
  36. package/src/sdk/bus/notification-service.ts +140 -7
  37. package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
  38. package/src/sdk/session.ts +24 -7
  39. package/src/session/agent-session.ts +176 -34
  40. package/src/session/internal/managed-session-storage.ts +280 -248
  41. package/src/session/internal/native-publish-outcome.ts +12 -2
  42. package/src/session/session-manager.ts +140 -6
  43. package/src/session/session-storage.ts +3 -0
  44. package/src/slash-commands/builtin-registry.ts +35 -11
  45. package/src/system-prompt.ts +28 -15
  46. package/src/tools/bash.ts +3 -1
  47. package/src/tools/composer-bash-policy.ts +21 -5
  48. package/src/tools/computer.ts +322 -45
  49. package/src/tools/todo-write.ts +29 -11
  50. package/src/utils/command-args.ts +19 -30
  51. package/src/utils/lang-from-path.ts +2 -4
@@ -99,7 +99,7 @@ export interface ComputerScreenshotDetails {
99
99
  originX?: number;
100
100
  originY?: number;
101
101
  displayEpoch?: number;
102
- captureId?: string;
102
+ captureId?: number;
103
103
  pngBytes?: number;
104
104
  path?: string;
105
105
  }
@@ -119,12 +119,50 @@ export interface ComputerToolDetails {
119
119
  keys?: string[];
120
120
  ms?: number;
121
121
  screenshot?: ComputerScreenshotDetails;
122
+ primaryError?: { code: string; message: string };
122
123
  supervisor?: string;
123
124
  steps?: ComputerToolDetails[];
124
125
  meta?: OutputMeta;
125
126
  }
126
127
 
128
+ type NativeBatchAction = {
129
+ action: "screenshot" | "click" | "double_click" | "move" | "drag" | "scroll" | "type" | "keypress" | "wait";
130
+ x?: number;
131
+ y?: number;
132
+ toX?: number;
133
+ toY?: number;
134
+ scrollX?: number;
135
+ scrollY?: number;
136
+ button?: string;
137
+ text?: string;
138
+ keys?: string[];
139
+ ms?: number;
140
+ timeoutMs?: number;
141
+ timeoutGroup?: number;
142
+ };
143
+
144
+ type NativeBatchStepResult = {
145
+ index: number;
146
+ action: string;
147
+ screenshot?: NativeScreenshot;
148
+ };
149
+
150
+ type NativeBatchResult = {
151
+ results: NativeBatchStepResult[];
152
+ failureCode?: string;
153
+ failureIndex?: number;
154
+ failureMessage?: string;
155
+ primaryFailureCode?: string;
156
+ primaryFailureMessage?: string;
157
+ };
158
+
127
159
  type NativeController = {
160
+ executeBatch?: (
161
+ expectedEpoch: number | undefined,
162
+ actions: NativeBatchAction[],
163
+ timeoutMs?: number,
164
+ signal?: AbortSignal,
165
+ ) => Promise<NativeBatchResult> | NativeBatchResult;
128
166
  screenshot?: () => Promise<NativeScreenshot> | NativeScreenshot;
129
167
  click?: (expectedEpoch: number | undefined, x: number, y: number, button?: string) => void;
130
168
  doubleClick?: (expectedEpoch: number | undefined, x: number, y: number, button?: string) => void;
@@ -145,7 +183,7 @@ type NativeScreenshot = {
145
183
  originX?: number;
146
184
  originY?: number;
147
185
  displayEpoch?: number;
148
- captureId?: string;
186
+ captureId?: number;
149
187
  };
150
188
 
151
189
  export type ComputerControllerFactory = () => NativeController;
@@ -183,7 +221,7 @@ const COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES = 5 * 1024 * 1024;
183
221
  const COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY = 70;
184
222
 
185
223
  export function setComputerControllerFactoryForTests(factory: ComputerControllerFactory | undefined): void {
186
- controllerFactory = factory ?? createNativeComputerController;
224
+ controllerFactory = factory ? () => withLegacyBatchAdapterForTests(factory()) : createNativeComputerController;
187
225
  }
188
226
 
189
227
  export function setComputerPlatformForTests(platform: NodeJS.Platform | undefined): void {
@@ -298,6 +336,10 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
298
336
  if (batchResult.failedStep) {
299
337
  details.code = batchResult.failedStep.code;
300
338
  details.message = batchResult.failedStep.message;
339
+ if (batchResult.primaryError) {
340
+ details.primaryError = batchResult.primaryError;
341
+ details.message += ` Primary failure: ${batchResult.primaryError.code}: ${batchResult.primaryError.message}`;
342
+ }
301
343
  if (batchResult.screenshotSource !== undefined) {
302
344
  await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
303
345
  }
@@ -320,6 +362,57 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
320
362
  .done()
321
363
  : toolResult(details).text(details.message).done();
322
364
  }
365
+ if (params.action !== "screenshot") {
366
+ const singleResult = await dispatchBatchComputerActions(
367
+ controller,
368
+ [params],
369
+ timeoutMs,
370
+ hotkey,
371
+ latestScreenshotContexts.get(this.session),
372
+ false,
373
+ Boolean(this.session.settings.get("computer.autoScreenshot")),
374
+ signal,
375
+ deadline,
376
+ );
377
+ if (singleResult.screenshot) {
378
+ details.screenshot = singleResult.screenshot;
379
+ rememberLatestScreenshot(this.session, singleResult.screenshot);
380
+ }
381
+ if (singleResult.failedStep) {
382
+ details.status = singleResult.failedStep.code === COMPUTER_DISABLED_CODE ? "disabled" : "error";
383
+ details.code = singleResult.failedStep.code;
384
+ details.message = singleResult.failedStep.message;
385
+ if (singleResult.primaryError) {
386
+ details.primaryError = singleResult.primaryError;
387
+ details.message += ` Primary failure: ${singleResult.primaryError.code}: ${singleResult.primaryError.message}`;
388
+ }
389
+ if (singleResult.screenshotSource !== undefined) {
390
+ await persistScreenshotFallback(singleResult.screenshotSource, details.screenshot, this.session);
391
+ }
392
+ await writeComputerAuditLog(this.session, details);
393
+ return {
394
+ ...toolResult(details).text(`${details.code}: ${details.message}`).done(),
395
+ isError: true,
396
+ };
397
+ }
398
+ details.status = "success";
399
+ details.message = describeComputerSuccess(details);
400
+ if (singleResult.screenshotSource !== undefined) {
401
+ await persistScreenshotFallback(singleResult.screenshotSource, details.screenshot, this.session);
402
+ details.message = describeComputerSuccess(details);
403
+ }
404
+ const image = await inlineImageContentFromNativeResult(
405
+ singleResult.screenshotSource,
406
+ details,
407
+ this.session,
408
+ );
409
+ await writeComputerAuditLog(this.session, details);
410
+ return image
411
+ ? toolResult(details)
412
+ .content([{ type: "text", text: details.message }, image])
413
+ .done()
414
+ : toolResult(details).text(details.message).done();
415
+ }
323
416
  let result = await dispatchComputerAction(
324
417
  controller,
325
418
  params,
@@ -509,6 +602,85 @@ async function runComputerOperation<T>(
509
602
  }
510
603
  }
511
604
 
605
+ function withLegacyBatchAdapterForTests(controller: NativeController): NativeController {
606
+ if (
607
+ controller.executeBatch ||
608
+ (!controller.click &&
609
+ !controller.doubleClick &&
610
+ !controller.move &&
611
+ !controller.drag &&
612
+ !controller.scroll &&
613
+ !controller.type &&
614
+ !controller.keypress &&
615
+ !controller.wait)
616
+ ) {
617
+ return controller;
618
+ }
619
+ return {
620
+ ...controller,
621
+ executeBatch: async (expectedEpoch, actions, _timeoutMs, signal) => {
622
+ const results: NativeBatchStepResult[] = [];
623
+ for (const [index, action] of actions.entries()) {
624
+ throwIfAborted(signal);
625
+ switch (action.action) {
626
+ case "screenshot":
627
+ if (!controller.screenshot) missingNativeMethod("screenshot", "screenshot");
628
+ results.push({ index, action: "screenshot", screenshot: await controller.screenshot() });
629
+ break;
630
+ case "click":
631
+ if (!controller.click) missingNativeMethod("click", "click");
632
+ controller.click(expectedEpoch, action.x!, action.y!, action.button ?? "left");
633
+ results.push({ index, action: "click" });
634
+ break;
635
+ case "double_click":
636
+ if (!controller.doubleClick) missingNativeMethod("double_click", "doubleClick");
637
+ controller.doubleClick(expectedEpoch, action.x!, action.y!, action.button ?? "left");
638
+ results.push({ index, action: "double_click" });
639
+ break;
640
+ case "move":
641
+ if (!controller.move) missingNativeMethod("move", "move");
642
+ controller.move(expectedEpoch, action.x!, action.y!);
643
+ results.push({ index, action: "move" });
644
+ break;
645
+ case "drag":
646
+ if (!controller.drag) missingNativeMethod("drag", "drag");
647
+ controller.drag(
648
+ expectedEpoch,
649
+ action.x!,
650
+ action.y!,
651
+ action.toX!,
652
+ action.toY!,
653
+ action.button ?? "left",
654
+ );
655
+ results.push({ index, action: "drag" });
656
+ break;
657
+ case "scroll":
658
+ if (!controller.scroll) missingNativeMethod("scroll", "scroll");
659
+ controller.scroll(expectedEpoch, action.x!, action.y!, action.scrollX!, action.scrollY!);
660
+ results.push({ index, action: "scroll" });
661
+ break;
662
+ case "type":
663
+ if (!controller.type) missingNativeMethod("type", "type");
664
+ controller.type(undefined, action.text!);
665
+ results.push({ index, action: "type" });
666
+ break;
667
+ case "keypress":
668
+ if (!controller.keypress) missingNativeMethod("keypress", "keypress");
669
+ controller.keypress(undefined, action.keys!);
670
+ results.push({ index, action: "keypress" });
671
+ break;
672
+ case "wait":
673
+ if (!controller.wait) missingNativeMethod("wait", "wait");
674
+ controller.wait(undefined, action.ms!);
675
+ results.push({ index, action: "wait" });
676
+ break;
677
+ }
678
+ }
679
+ return { results };
680
+ },
681
+ };
682
+ }
683
+
512
684
  function dispatchComputerAction(
513
685
  controller: NativeController,
514
686
  params: SingleComputerParams,
@@ -572,6 +744,64 @@ interface BatchDispatchResult {
572
744
  screenshot?: ComputerScreenshotDetails;
573
745
  screenshotSource?: unknown;
574
746
  failedStep?: { code: string; message: string };
747
+ primaryError?: { code: string; message: string };
748
+ }
749
+
750
+ interface BatchWireStep {
751
+ action: NativeBatchAction;
752
+ userIndex?: number;
753
+ finalScreenshot?: boolean;
754
+ }
755
+
756
+ function nativeBatchAction(params: SingleComputerParams, timeoutMs: number | undefined): NativeBatchAction {
757
+ const actionTimeoutMs = stepTimeoutFromParams(params, timeoutMs);
758
+ switch (params.action) {
759
+ case "screenshot":
760
+ return { action: "screenshot", timeoutMs: actionTimeoutMs };
761
+ case "click":
762
+ case "double_click":
763
+ case "move":
764
+ return {
765
+ action: params.action,
766
+ x: params.x,
767
+ y: params.y,
768
+ button: params.button,
769
+ timeoutMs: actionTimeoutMs,
770
+ };
771
+ case "drag":
772
+ return {
773
+ action: "drag",
774
+ x: params.x,
775
+ y: params.y,
776
+ toX: params.to_x,
777
+ toY: params.to_y,
778
+ button: params.button,
779
+ timeoutMs: actionTimeoutMs,
780
+ };
781
+ case "scroll":
782
+ return {
783
+ action: "scroll",
784
+ x: params.x,
785
+ y: params.y,
786
+ scrollX: params.scroll_x,
787
+ scrollY: params.scroll_y,
788
+ timeoutMs: actionTimeoutMs,
789
+ };
790
+ case "type":
791
+ return { action: "type", text: params.text, timeoutMs: actionTimeoutMs };
792
+ case "keypress":
793
+ return { action: "keypress", keys: params.keys, timeoutMs: actionTimeoutMs };
794
+ case "wait":
795
+ return { action: "wait", ms: capWaitMs(params.ms, actionTimeoutMs), timeoutMs: actionTimeoutMs };
796
+ }
797
+ }
798
+
799
+ function mapNativeBatchFailure(
800
+ code: string,
801
+ message: string | undefined,
802
+ hotkey?: string,
803
+ ): { code: string; message: string } {
804
+ return mapComputerError({ code, message: message ?? `${code}: Computer batch failed.` }, hotkey);
575
805
  }
576
806
 
577
807
  async function dispatchBatchComputerActions(
@@ -585,58 +815,100 @@ async function dispatchBatchComputerActions(
585
815
  signal?: AbortSignal,
586
816
  deadline?: ComputerDeadline,
587
817
  ): Promise<BatchDispatchResult> {
588
- const steps: ComputerToolDetails[] = [];
818
+ if (!controller.executeBatch) missingNativeMethod("batch", "executeBatch");
819
+
820
+ const steps = actions.map(detailsFromParams);
821
+ const wireSteps: BatchWireStep[] = [];
822
+ for (const [userIndex, action] of actions.entries()) {
823
+ const nativeAction = { ...nativeBatchAction(action, timeoutMs), timeoutGroup: userIndex };
824
+ wireSteps.push({ action: nativeAction, userIndex });
825
+ if (action.action !== "screenshot" && (action.include_screenshot === true || autoScreenshot)) {
826
+ wireSteps.push({
827
+ action: {
828
+ action: "screenshot",
829
+ timeoutMs: nativeAction.timeoutMs,
830
+ timeoutGroup: nativeAction.timeoutGroup,
831
+ },
832
+ userIndex,
833
+ });
834
+ }
835
+ }
836
+ if (includeBatchScreenshot) {
837
+ wireSteps.push({ action: { action: "screenshot", timeoutMs }, finalScreenshot: true });
838
+ }
839
+
840
+ throwIfAborted(signal);
841
+ assertComputerDeadline(deadline);
842
+ const nativeTimeoutMs = remainingComputerTimeoutMs(deadline);
843
+ const nativeResult = await controller.executeBatch(
844
+ expectedEpochFromContext(initialContext),
845
+ wireSteps.map(step => step.action),
846
+ nativeTimeoutMs,
847
+ signal,
848
+ );
589
849
  let lastScreenshot: ComputerScreenshotDetails | undefined;
590
850
  let lastScreenshotSource: unknown;
591
- let context = initialContext;
592
- for (const single of actions) {
593
- const stepDetails = detailsFromParams(single);
594
- try {
595
- throwIfAborted(signal);
596
- assertComputerDeadline(deadline);
597
- const stepTimeoutMs = stepTimeoutFromParams(single, timeoutMs);
598
- const stepDeadline = createComputerDeadline(stepTimeoutMs, deadline);
599
- let result = await dispatchComputerAction(controller, single, stepDeadline, context, signal);
600
- if (single.action !== "screenshot" && (single.include_screenshot === true || autoScreenshot)) {
601
- result = await captureScreenshot(controller, stepDeadline, signal);
602
- }
603
-
604
- const screenshot = normalizeScreenshot(result);
605
- if (screenshot) {
606
- stepDetails.screenshot = screenshot;
607
- lastScreenshot = screenshot;
608
- lastScreenshotSource = result;
609
- context = screenshot;
610
- }
611
- stepDetails.status = "success";
612
- stepDetails.message = describeComputerSuccess(stepDetails);
613
- } catch (error) {
614
- if (error instanceof ToolAbortError) throw error;
615
- const mapped = mapComputerError(error, hotkey);
616
- stepDetails.status = mapped.code === COMPUTER_DISABLED_CODE ? "disabled" : "error";
617
- stepDetails.code = mapped.code;
618
- stepDetails.message = mapped.message;
619
- steps.push(stepDetails);
620
- return {
621
- steps,
622
- screenshot: lastScreenshot,
623
- screenshotSource: lastScreenshotSource,
624
- failedStep: { code: mapped.code, message: mapped.message },
625
- };
851
+ const completedUserIndices = new Set<number>();
852
+ for (const result of nativeResult.results) {
853
+ const wireStep = wireSteps[result.index];
854
+ if (!wireStep) continue;
855
+ if (wireStep.userIndex !== undefined) completedUserIndices.add(wireStep.userIndex);
856
+ const screenshot = normalizeScreenshot(result);
857
+ if (screenshot) {
858
+ lastScreenshot = screenshot;
859
+ lastScreenshotSource = result.screenshot;
860
+ if (wireStep.userIndex !== undefined) steps[wireStep.userIndex]!.screenshot = screenshot;
626
861
  }
627
- steps.push(stepDetails);
628
862
  }
629
- if (includeBatchScreenshot) {
630
- lastScreenshotSource = await captureScreenshot(controller, deadline, signal);
631
- lastScreenshot = normalizeScreenshot(lastScreenshotSource) ?? lastScreenshot;
863
+ if (!nativeResult.failureCode) {
864
+ throwIfAborted(signal);
865
+ assertComputerDeadline(deadline);
866
+ for (const step of steps) step.message = describeComputerSuccess(step);
867
+ return { steps, screenshot: lastScreenshot, screenshotSource: lastScreenshotSource };
632
868
  }
633
- return { steps, screenshot: lastScreenshot, screenshotSource: lastScreenshotSource };
869
+
870
+ const mapped = mapNativeBatchFailure(nativeResult.failureCode, nativeResult.failureMessage, hotkey);
871
+ const failedWireStep = wireSteps[nativeResult.failureIndex ?? nativeResult.results.length];
872
+ const primaryError =
873
+ nativeResult.primaryFailureCode === undefined
874
+ ? undefined
875
+ : {
876
+ code: nativeResult.primaryFailureCode,
877
+ message: nativeResult.primaryFailureMessage ?? "Computer batch failed.",
878
+ };
879
+ if (failedWireStep?.userIndex !== undefined) {
880
+ const failed = steps[failedWireStep.userIndex]!;
881
+ failed.status = mapped.code === COMPUTER_DISABLED_CODE ? "disabled" : "error";
882
+ failed.code = mapped.code;
883
+ failed.message = mapped.message;
884
+ }
885
+ for (const index of completedUserIndices) {
886
+ const step = steps[index];
887
+ if (step && !step.message && step.status === "success") {
888
+ step.message = describeComputerSuccess(step);
889
+ }
890
+ }
891
+ const reportedIndices = new Set(completedUserIndices);
892
+ if (failedWireStep?.userIndex !== undefined) reportedIndices.add(failedWireStep.userIndex);
893
+ const reportedSteps = [...reportedIndices]
894
+ .sort((left, right) => left - right)
895
+ .map(index => steps[index]!)
896
+ .filter(Boolean);
897
+ return {
898
+ steps: reportedSteps,
899
+ screenshot: lastScreenshot,
900
+ screenshotSource: lastScreenshotSource,
901
+ failedStep: mapped,
902
+ primaryError,
903
+ };
634
904
  }
635
905
 
636
906
  function stepTimeoutFromParams(params: SingleComputerParams, batchTimeoutMs: number | undefined): number | undefined {
637
907
  if (params.timeout === undefined) return batchTimeoutMs;
638
908
  const timeoutSeconds = clampTimeout("computer", params.timeout);
639
- return timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined;
909
+ const stepTimeoutMs = timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined;
910
+ if (stepTimeoutMs === undefined || batchTimeoutMs === undefined) return stepTimeoutMs ?? batchTimeoutMs;
911
+ return Math.min(stepTimeoutMs, batchTimeoutMs);
640
912
  }
641
913
 
642
914
  function detailsFromParams(params: ComputerParams): ComputerToolDetails {
@@ -821,6 +1093,9 @@ function mapComputerError(error: unknown, hotkey?: string): { code: string; mess
821
1093
  "The host needs screen-recording or accessibility permission. Ask the user to grant it.",
822
1094
  COMPUTER_DISABLED:
823
1095
  "The computer tool is disabled or unsupported. Do not retry without enabling it on Apple Silicon macOS.",
1096
+ COMPUTER_SCREENSHOT_FAILED: "Capture failed. Check screen-recording permission and retry the screenshot.",
1097
+ COMPUTER_CURSOR_RESTORE_FAILED:
1098
+ "Input may have completed, but the cursor could not be restored. Stop and ask the user to inspect the desktop before retrying.",
824
1099
  };
825
1100
  const hint = recoveryHints[code];
826
1101
  const message = hint ? `${code}: ${reason} ${hint}` : `${code}: ${reason}`;
@@ -832,6 +1107,7 @@ interface ComputerAuditRecord {
832
1107
  action: ComputerActionName;
833
1108
  status: "success" | "error" | "disabled";
834
1109
  code?: string;
1110
+ primaryError?: { code: string; message: string };
835
1111
  x?: number;
836
1112
  y?: number;
837
1113
  toX?: number;
@@ -853,6 +1129,7 @@ function auditRecordFromDetails(details: ComputerToolDetails): ComputerAuditReco
853
1129
  status: details.status,
854
1130
  };
855
1131
  if (details.code) record.code = details.code;
1132
+ if (details.primaryError) record.primaryError = details.primaryError;
856
1133
  if (details.x !== undefined) record.x = details.x;
857
1134
  if (details.y !== undefined) record.y = details.y;
858
1135
  if (details.toX !== undefined) record.toX = details.toX;
@@ -39,6 +39,7 @@ export interface TodoPhase {
39
39
  export interface TodoWriteToolDetails {
40
40
  phases: TodoPhase[];
41
41
  storage: "session" | "memory";
42
+ failureKind?: "payload_rejected" | "persistence";
42
43
  }
43
44
 
44
45
  // =============================================================================
@@ -520,6 +521,9 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
520
521
  }
521
522
  return lines.join("\n");
522
523
  }
524
+ function formatPayloadRejectedSummary(phases: TodoPhase[], errors: string[]): string {
525
+ return `${formatSummary(phases, errors)}\nTodo update was not applied.`;
526
+ }
523
527
 
524
528
  // =============================================================================
525
529
  // Tool Class
@@ -547,26 +551,32 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
547
551
  _context?: AgentToolContext,
548
552
  ): Promise<AgentToolResult<TodoWriteToolDetails>> {
549
553
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
554
+ const storage = this.session.getSessionFile() ? "session" : "memory";
550
555
  const missingTarget = params.ops.find(
551
556
  entry => (entry.op === "done" || entry.op === "drop") && !entry.task && !entry.phase,
552
557
  );
553
558
  if (missingTarget) {
554
- const storage = this.session.getSessionFile() ? "session" : "memory";
555
559
  const errors = [`Missing task or phase for ${missingTarget.op} operation`];
556
560
  return {
557
- content: [{ type: "text", text: formatSummary(previousPhases, errors) }],
558
- details: { phases: previousPhases, storage },
561
+ content: [{ type: "text", text: formatPayloadRejectedSummary(previousPhases, errors) }],
562
+ details: { phases: previousPhases, storage, failureKind: "payload_rejected" },
559
563
  isError: true,
560
564
  };
561
565
  }
562
- const { phases: updated, errors } = applyParams(previousPhases, params);
563
- this.session.setTodoPhases?.(updated);
564
- const storage = this.session.getSessionFile() ? "session" : "memory";
566
+ const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
567
+ const rejected = errors.length > 0;
568
+ if (!rejected) this.session.setTodoPhases?.(updated);
569
+ const phases = rejected ? previousPhases : updated;
565
570
 
566
571
  return {
567
- content: [{ type: "text", text: formatSummary(updated, errors) }],
568
- details: { phases: updated, storage },
569
- isError: errors.length > 0 ? true : undefined,
572
+ content: [
573
+ {
574
+ type: "text",
575
+ text: rejected ? formatPayloadRejectedSummary(phases, errors) : formatSummary(phases, errors),
576
+ },
577
+ ],
578
+ details: rejected ? { phases, storage, failureKind: "payload_rejected" } : { phases, storage },
579
+ isError: rejected || undefined,
570
580
  };
571
581
  }
572
582
  }
@@ -698,19 +708,27 @@ export const todoWriteToolRenderer = {
698
708
  },
699
709
 
700
710
  renderResult(
701
- result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails },
711
+ result: {
712
+ content: Array<{ type: string; text?: string }>;
713
+ details?: TodoWriteToolDetails;
714
+ isError?: boolean;
715
+ },
702
716
  options: RenderResultOptions,
703
717
  uiTheme: Theme,
704
718
  _args?: TodoWriteRenderArgs,
705
719
  ): Component {
706
720
  const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
707
721
  const allTasks = phases.flatMap(phase => phase.tasks);
722
+ const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
723
+ if (result.isError) {
724
+ const header = renderStatusLine({ icon: "error", title: "Todo Write" }, uiTheme);
725
+ return new Text(`${header}\n${uiTheme.fg("error", fallback)}`, 0, 0);
726
+ }
708
727
  const header = renderStatusLine(
709
728
  { icon: "success", title: "Todo Write", meta: [`${allTasks.length} tasks`] },
710
729
  uiTheme,
711
730
  );
712
731
  if (allTasks.length === 0) {
713
- const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
714
732
  return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
715
733
  }
716
734
 
@@ -39,38 +39,27 @@ export function parseCommandArgs(argsString: string): string[] {
39
39
  * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.
40
40
  */
41
41
  export function substituteArgs(content: string, args: string[]): string {
42
- let result = content;
43
-
44
- // Replace $1, $2, etc. with positional args FIRST (before wildcards)
45
- // This prevents wildcard replacement values containing $<digit> patterns from being re-substituted
46
- result = result.replace(/\$(\d+)/g, (_, num) => {
47
- const index = parseInt(num, 10) - 1;
48
- return args[index] ?? "";
49
- });
50
-
51
- result = result.replace(/\$@\[(\d+)(?::(\d*)?)?\]/g, (_, startRaw: string, lengthRaw?: string) => {
52
- const start = Number.parseInt(startRaw, 10);
53
- if (!Number.isFinite(start) || start < 1) return "";
54
- const startIndex = start - 1;
55
- if (startIndex >= args.length) return "";
56
-
57
- if (lengthRaw === undefined || lengthRaw === "") {
58
- return args.slice(startIndex).join(" ");
59
- }
60
-
61
- const length = Number.parseInt(lengthRaw, 10);
62
- if (!Number.isFinite(length) || length <= 0) return "";
63
- return args.slice(startIndex, startIndex + length).join(" ");
64
- });
65
-
66
- // Pre-compute all args joined (optimization)
67
42
  const allArgs = args.join(" ");
43
+ return content.replace(
44
+ /\$@\[(\d+)(?::(\d*)?)?\]|\$ARGUMENTS|\$@|\$(\d+)/g,
45
+ (_, startRaw: string | undefined, lengthRaw: string | undefined, positionRaw: string | undefined) => {
46
+ if (positionRaw !== undefined) {
47
+ return args[Number.parseInt(positionRaw, 10) - 1] ?? "";
48
+ }
49
+ if (startRaw === undefined) return allArgs;
68
50
 
69
- // Replace $ARGUMENTS with all args joined (new syntax, aligns with Anthropic model, OpenAI code backend, OpenCode)
70
- result = result.replaceAll("$ARGUMENTS", allArgs);
51
+ const start = Number.parseInt(startRaw, 10);
52
+ if (!Number.isFinite(start) || start < 1) return "";
53
+ const startIndex = start - 1;
54
+ if (startIndex >= args.length) return "";
71
55
 
72
- // Replace $@ with all args joined (existing syntax)
73
- result = result.replaceAll("$@", allArgs);
56
+ if (lengthRaw === undefined || lengthRaw === "") {
57
+ return args.slice(startIndex).join(" ");
58
+ }
74
59
 
75
- return result;
60
+ const length = Number.parseInt(lengthRaw, 10);
61
+ if (!Number.isFinite(length) || length <= 0) return "";
62
+ return args.slice(startIndex, startIndex + length).join(" ");
63
+ },
64
+ );
76
65
  }
@@ -201,18 +201,16 @@ function lspExtensionKey(filePath: string): string {
201
201
  * Language id for syntax highlighting and UI (icons, read tool), or undefined if unknown.
202
202
  */
203
203
  export function getLanguageFromPath(filePath: string): string | undefined {
204
- const pair = EXTENSION_LANG[themeExtensionKey(filePath)];
205
- if (pair) return pair[0];
206
-
207
204
  const baseName = path.basename(filePath).toLowerCase();
208
205
  if (baseName.startsWith(".env.")) return "env";
209
206
  if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
210
207
  return "dockerfile";
211
208
  }
209
+ if (baseName === "makefile" || baseName === "gnumakefile") return "make";
212
210
  if (baseName === "justfile") return "just";
213
211
  if (baseName === "cmakelists.txt") return "cmake";
214
212
 
215
- return undefined;
213
+ return EXTENSION_LANG[themeExtensionKey(filePath)]?.[0];
216
214
  }
217
215
 
218
216
  /**