@gajae-code/coding-agent 0.16.0 → 0.16.3

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 (164) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/update-cli.d.ts +1 -0
  5. package/dist/types/commands/sdk.d.ts +2 -0
  6. package/dist/types/config/billing-path.d.ts +25 -0
  7. package/dist/types/config/config-schema-version.d.ts +13 -1
  8. package/dist/types/config/model-registry.d.ts +16 -0
  9. package/dist/types/config/settings-schema.d.ts +17 -7
  10. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  11. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  12. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  15. package/dist/types/modes/components/model-selector.d.ts +4 -0
  16. package/dist/types/modes/components/session-selector.d.ts +1 -1
  17. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  18. package/dist/types/modes/interactive-mode.d.ts +16 -2
  19. package/dist/types/modes/theme/theme.d.ts +2 -1
  20. package/dist/types/modes/types.d.ts +2 -0
  21. package/dist/types/sdk/broker/broker.d.ts +2 -1
  22. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
  25. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  26. package/dist/types/sdk/broker/state-version.d.ts +17 -0
  27. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  28. package/dist/types/sdk/bus/index.d.ts +3 -1
  29. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  30. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  31. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  32. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  33. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  34. package/dist/types/sdk/cli/rows.d.ts +15 -2
  35. package/dist/types/sdk/cli/session-cli.d.ts +2 -1
  36. package/dist/types/sdk/host/events.d.ts +2 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  39. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  40. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  41. package/dist/types/sdk/host/session-runtime.d.ts +18 -17
  42. package/dist/types/sdk/lifecycle-session.d.ts +1 -1
  43. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  44. package/dist/types/sdk/receipt-state.d.ts +2 -0
  45. package/dist/types/sdk/router/session-router.d.ts +4 -0
  46. package/dist/types/sdk/turn-result.d.ts +6 -0
  47. package/dist/types/session/agent-session.d.ts +47 -7
  48. package/dist/types/session/fold-coordinator.d.ts +29 -2
  49. package/dist/types/session/session-manager.d.ts +14 -2
  50. package/dist/types/tools/ask.d.ts +1 -0
  51. package/dist/types/tools/bash.d.ts +12 -0
  52. package/dist/types/tools/browser/launch.d.ts +10 -0
  53. package/dist/types/tools/browser/registry.d.ts +8 -0
  54. package/dist/types/tools/index.d.ts +4 -10
  55. package/dist/types/tools/job.d.ts +3 -1
  56. package/dist/types/utils/external-editor.d.ts +12 -0
  57. package/examples/extensions/README.md +1 -1
  58. package/examples/extensions/pirate.ts +7 -4
  59. package/package.json +7 -7
  60. package/scripts/generate-sdk-operation-inventory.ts +7 -1
  61. package/src/async/job-manager.ts +107 -9
  62. package/src/browser-backend.ts +29 -0
  63. package/src/cli/plugin-cli.ts +29 -0
  64. package/src/cli/update-cli.ts +63 -12
  65. package/src/commands/sdk.ts +22 -7
  66. package/src/config/billing-path.ts +42 -0
  67. package/src/config/config-schema-version.ts +18 -1
  68. package/src/config/model-preset-registry.ts +177 -18
  69. package/src/config/model-registry.ts +268 -42
  70. package/src/config/settings-schema.ts +19 -7
  71. package/src/config/settings.ts +46 -15
  72. package/src/coordinator-mcp/projection-scan.ts +6 -6
  73. package/src/coordinator-mcp/server.ts +21 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/extensibility/extensions/runner.ts +27 -0
  76. package/src/extensibility/extensions/types.ts +8 -3
  77. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  78. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  79. package/src/extensibility/skills.ts +22 -12
  80. package/src/gjc-runtime/launch-worktree.ts +285 -4
  81. package/src/gjc-runtime/session-state-lock.ts +156 -32
  82. package/src/gjc-runtime/session-state-sidecar.ts +10 -17
  83. package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
  84. package/src/internal-urls/docs-index.generated.ts +11 -11
  85. package/src/main.ts +4 -1
  86. package/src/modes/acp/acp-agent.ts +34 -19
  87. package/src/modes/components/model-selector.ts +76 -8
  88. package/src/modes/components/session-selector.ts +23 -15
  89. package/src/modes/components/sessions-dashboard.ts +8 -5
  90. package/src/modes/controllers/command-controller.ts +1 -1
  91. package/src/modes/controllers/event-controller.ts +30 -4
  92. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  93. package/src/modes/controllers/input-controller.ts +2 -2
  94. package/src/modes/controllers/selector-controller.ts +10 -3
  95. package/src/modes/interactive-mode.ts +36 -12
  96. package/src/modes/prompt-action-autocomplete.ts +30 -4
  97. package/src/modes/runtime-init.ts +12 -9
  98. package/src/modes/theme/theme.ts +5 -0
  99. package/src/modes/types.ts +2 -0
  100. package/src/modes/utils/ui-helpers.ts +22 -3
  101. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  102. package/src/prompts/tools/ask.md +1 -1
  103. package/src/prompts/tools/bash.md +1 -0
  104. package/src/prompts/tools/job.md +1 -1
  105. package/src/sdk/acp/adapter.ts +6 -1
  106. package/src/sdk/broker/broker.ts +10 -1
  107. package/src/sdk/broker/ensure.ts +76 -3
  108. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  109. package/src/sdk/broker/lifecycle.ts +254 -34
  110. package/src/sdk/broker/spawn-substrate.ts +45 -2
  111. package/src/sdk/broker/startup-budget.ts +144 -2
  112. package/src/sdk/broker/state-version.ts +17 -0
  113. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  114. package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
  115. package/src/sdk/bus/discord-daemon.ts +50 -10
  116. package/src/sdk/bus/index.ts +33 -3
  117. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  118. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  119. package/src/sdk/bus/notification-service.ts +22 -5
  120. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  121. package/src/sdk/bus/reconciliation-store.ts +55 -31
  122. package/src/sdk/bus/slack-conversation.ts +1 -1
  123. package/src/sdk/bus/slack-daemon.ts +72 -38
  124. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  125. package/src/sdk/cli/rows.ts +75 -7
  126. package/src/sdk/cli/session-cli.ts +68 -35
  127. package/src/sdk/host/control/dispatch.ts +2 -2
  128. package/src/sdk/host/events.ts +26 -2
  129. package/src/sdk/host/host.ts +6 -1
  130. package/src/sdk/host/query/cursor.ts +52 -7
  131. package/src/sdk/host/query/handlers.ts +33 -12
  132. package/src/sdk/host/query/revision-store.ts +12 -0
  133. package/src/sdk/host/session-runtime.test.ts +497 -31
  134. package/src/sdk/host/session-runtime.ts +387 -54
  135. package/src/sdk/host/surface-policy.ts +3 -3
  136. package/src/sdk/lifecycle-session.ts +7 -2
  137. package/src/sdk/prompt-deadline-manager.ts +2 -1
  138. package/src/sdk/protocol/operation-inventory.generated.json +54 -3
  139. package/src/sdk/protocol/operation-registry.ts +1 -1
  140. package/src/sdk/receipt-state.ts +10 -0
  141. package/src/sdk/router/session-router.ts +210 -94
  142. package/src/sdk/session.ts +59 -41
  143. package/src/sdk/turn-result.ts +22 -0
  144. package/src/session/agent-session.ts +1033 -260
  145. package/src/session/fold-coordinator.ts +40 -3
  146. package/src/session/session-manager.ts +517 -77
  147. package/src/slash-commands/builtin-registry.ts +41 -0
  148. package/src/task/executor.ts +9 -4
  149. package/src/tools/ask-contract.test.ts +64 -0
  150. package/src/tools/ask-contract.ts +36 -1
  151. package/src/tools/ask.ts +10 -8
  152. package/src/tools/bash-interactive.ts +11 -2
  153. package/src/tools/bash.ts +158 -33
  154. package/src/tools/browser/launch.ts +37 -0
  155. package/src/tools/browser/registry.ts +99 -9
  156. package/src/tools/cron.ts +6 -4
  157. package/src/tools/descriptors.test.ts +15 -0
  158. package/src/tools/descriptors.ts +5 -1
  159. package/src/tools/index.ts +4 -6
  160. package/src/tools/job.ts +4 -1
  161. package/src/tools/monitor.ts +12 -9
  162. package/src/tools/resolve.ts +30 -10
  163. package/src/tools/tool-catalog.generated.ts +2 -2
  164. package/src/utils/external-editor.ts +86 -1
@@ -1,7 +1,7 @@
1
1
  import type { AgentTelemetryConfig, AgentTool } from "@gajae-code/agent-core";
2
2
  import type { Model, ServiceTier, ToolChoice } from "@gajae-code/ai/core";
3
3
  import { $env, logger } from "@gajae-code/utils";
4
- import type { AsyncJobManager } from "../async";
4
+ import type { AsyncJobManager, FoldReason } from "../async";
5
5
  import type { PromptTemplate } from "../config/prompt-templates";
6
6
  import type { Settings } from "../config/settings";
7
7
  import type { Skill } from "../extensibility/skills";
@@ -240,12 +240,12 @@ export interface ToolSession {
240
240
  registerForegroundFoldParticipant?: (adapter: FoldAdapter) => () => void;
241
241
  /** Whether a foreground wait is currently foldable into a background job. */
242
242
  hasForegroundBashBackgroundRequestHandler?: () => boolean;
243
- /** Request that the active foreground wait fold into a background job, if supported. */
244
- requestForegroundBashBackground?: () => Promise<boolean>;
243
+ /** Request that a foreground wait fold into a background job, if supported. `reason` names the trigger; `adapter` targets a specific wait instead of the newest registration. */
244
+ requestForegroundBashBackground?: (reason?: FoldReason, adapter?: FoldAdapter) => Promise<boolean>;
245
245
 
246
246
  /** Get the session-owned or inherited async job manager. */
247
247
  getAsyncJobManager?: () => AsyncJobManager | undefined;
248
- /** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
248
+ /** Resolves on a steer admitted after the wait starts, or when `signal` aborts, without consuming the steer. */
249
249
  waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
250
250
  /** Get session ID */
251
251
  getSessionId?: () => string | null;
@@ -379,8 +379,6 @@ export interface ToolSession {
379
379
  buildToolChoice?(toolName: string): ToolChoice | undefined;
380
380
  /** Build a named tool-choice decision, preserving whether exact named forcing survived capability degradation. */
381
381
  buildToolChoiceResult?(toolName: string): import("../utils/tool-choice").NamedToolChoiceResult;
382
- /** Steer a hidden custom message into the conversation (e.g. a preview reminder). */
383
- steer?(message: { customType: string; content: string; details?: unknown }): void;
384
382
  /** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
385
383
  peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
386
384
  /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
package/src/tools/job.ts CHANGED
@@ -3,7 +3,7 @@ import type { Component } from "@gajae-code/tui";
3
3
  import { Text } from "@gajae-code/tui";
4
4
  import { prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
6
+ import { type AsyncJob, AsyncJobManager, type FoldReason, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
9
  import jobDescription from "../prompts/tools/job.md" with { type: "text" };
@@ -50,6 +50,8 @@ interface JobSnapshot {
50
50
  status: "running" | "completed" | "failed" | "cancelled";
51
51
  label: string;
52
52
  durationMs: number;
53
+ /** Present when the job was folded out of a foreground wait. */
54
+ foldReason?: FoldReason;
53
55
  resultText?: string;
54
56
  errorText?: string;
55
57
  }
@@ -344,6 +346,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
344
346
  status: latest.status as JobSnapshot["status"],
345
347
  label: latest.label,
346
348
  durationMs: jobElapsedMs(latest, now),
349
+ ...(current?.metadata?.foldReason ? { foldReason: current.metadata.foldReason } : {}),
347
350
  ...(latest.resultText ? { resultText: latest.resultText } : {}),
348
351
  ...(latest.errorText ? { errorText: latest.errorText } : {}),
349
352
  };
@@ -229,19 +229,22 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
229
229
  );
230
230
  pendingNotifications = MAX_PENDING_MONITOR_NOTIFICATIONS;
231
231
  }
232
- const sendPromise = this.session.sendCustomMessage?.(
233
- { customType: "task-notification", content, display: false, attribution: "agent", details },
234
- { triggerTurn: true, deliverAs: "followUp" },
235
- );
236
- if (sendPromise) {
237
- void sendPromise.catch(error => {
232
+ if (!this.session.sendCustomMessage) {
233
+ logger.warn("Monitor task-notification dropped: session has no custom-message router", {
234
+ taskId: jobId,
235
+ });
236
+ return;
237
+ }
238
+ void this.session
239
+ .sendCustomMessage(
240
+ { customType: "task-notification", content, display: false, attribution: "agent", details },
241
+ { triggerTurn: true, deliverAs: "followUp" },
242
+ )
243
+ .catch(error => {
238
244
  logger.warn("Monitor task-notification delivery failed", {
239
245
  error: error instanceof Error ? error.message : String(error),
240
246
  });
241
247
  });
242
- } else {
243
- this.session.steer?.({ customType: "task-notification", content, details });
244
- }
245
248
  };
246
249
  const schedulePersistentNotification = (line: string) => {
247
250
  latestLine = line;
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { ToolChoice } from "@gajae-code/ai/core";
3
3
  import type { Component } from "@gajae-code/tui";
4
4
  import { Text } from "@gajae-code/tui";
5
- import { prompt, untilAborted } from "@gajae-code/utils";
5
+ import { logger, prompt, untilAborted } from "@gajae-code/utils";
6
6
  import * as z from "zod/v4";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
@@ -66,15 +66,35 @@ export function queueResolveHandler(
66
66
  const queue = session.getToolChoiceQueue?.();
67
67
 
68
68
  const steerReminder = (): void => {
69
- session.steer?.({
70
- customType: "resolve-reminder",
71
- content: [
72
- "<system-reminder>",
73
- "This is a preview. Call the `resolve` tool to apply or discard these changes.",
74
- "</system-reminder>",
75
- ].join("\n"),
76
- details: { toolName: options.sourceToolName },
77
- });
69
+ if (!session.sendCustomMessage) {
70
+ // The reminder is a degraded model's only model-visible route back to the
71
+ // standing resolve handler; a missing router must be visible.
72
+ logger.warn("Resolve reminder dropped: session has no custom-message router", {
73
+ toolName: options.sourceToolName,
74
+ });
75
+ return;
76
+ }
77
+ void session
78
+ .sendCustomMessage(
79
+ {
80
+ customType: "resolve-reminder",
81
+ content: [
82
+ "<system-reminder>",
83
+ "This is a preview. Call the `resolve` tool to apply or discard these changes.",
84
+ "</system-reminder>",
85
+ ].join("\n"),
86
+ display: false,
87
+ attribution: "agent",
88
+ details: { toolName: options.sourceToolName },
89
+ },
90
+ { deliverAs: "steer" },
91
+ )
92
+ .catch(error => {
93
+ logger.warn("Resolve reminder delivery failed", {
94
+ toolName: options.sourceToolName,
95
+ error: error instanceof Error ? error.message : String(error),
96
+ });
97
+ });
78
98
  };
79
99
 
80
100
  // Re-evaluated on every push (including apply-error re-pushes) so a runtime
@@ -283,7 +283,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
283
283
  "ask": {
284
284
  "name": "ask",
285
285
  "label": "Ask",
286
- "description": "Asks user when you need clarification or input during task execution.\n\n<conditions>\n- Multiple approaches exist with significantly different tradeoffs user should weigh\n</conditions>\n\n<instruction>\n- Use `recommended: <index>` to mark default (0-indexed); \" (Recommended)\" added automatically\n- Use `questions` for multiple related questions instead of asking one at a time\n- Set `multi: true` on question to allow multiple selections\n</instruction>\n\n<caution>\n- Provide 2-5 concise, distinct options\n</caution>\n\n<critical>\n- **Default to action.** Resolve ambiguity yourself using repo conventions, existing patterns, and reasonable defaults. Exhaust existing sources (code, configs, docs, history) before asking. Only ask when options have materially different tradeoffs the user must decide.\n- **If multiple choices are acceptable**, pick the most conservative/standard option and proceed; state the choice.\n- **Do NOT include \"Other\" option** — UI automatically adds \"Other (type your own)\" to every question.\n</critical>\n\n<examples>\n# Single question\nquestions: [{\"id\": \"auth_method\", \"question\": \"Which authentication method should this API use?\", \"options\": [{\"label\": \"JWT\"}, {\"label\": \"OAuth2\"}, {\"label\": \"Session cookies\"}], \"recommended\": 0}]\n\n# Multiple questions\nquestions: [{\"id\": \"storage_type\", \"question\": \"Which storage backend?\", \"options\": [{\"label\": \"SQLite\"}, {\"label\": \"PostgreSQL\"}]}, {\"id\": \"auth_method\", \"question\": \"Which auth method?\", \"options\": [{\"label\": \"JWT\"}, {\"label\": \"Session cookies\"}]}]\n</examples>",
286
+ "description": "Asks user when you need clarification or input during task execution.\n\n<conditions>\n- Multiple approaches exist with significantly different tradeoffs user should weigh\n</conditions>\n\n<instruction>\n- Use `recommended: <index>` to mark default (0-indexed); \" (Recommended)\" added automatically\n- Use `questions` for multiple related questions instead of asking one at a time\n- Set `multi: true` on question to allow multiple selections\n</instruction>\n\n<caution>\n- Provide 2-5 concise, distinct options\n</caution>\n\n<critical>\n- **Default to action.** Resolve ambiguity yourself using repo conventions, existing patterns, and reasonable defaults. Exhaust existing sources (code, configs, docs, history) before asking. Only ask when options have materially different tradeoffs the user must decide.\n- **If multiple choices are acceptable**, pick the most conservative/standard option and proceed; state the choice.\n- **Do NOT include the automatic custom-input control or labels that imitate it** such as \"Other (type your own)\", \"Other (specify)\", \"type your own\", \"custom input\", or \"직접 입력\" — UI automatically adds \"Other (type your own)\" to every question. A plain domain value such as \"Other\" is allowed when it is a genuine answer, not a request for free text. To collect free text, ask for it in the question body and rely on the automatic Other/custom-input control instead of adding a fake option.\n</critical>\n\n<examples>\n# Single question\nquestions: [{\"id\": \"auth_method\", \"question\": \"Which authentication method should this API use?\", \"options\": [{\"label\": \"JWT\"}, {\"label\": \"OAuth2\"}, {\"label\": \"Session cookies\"}], \"recommended\": 0}]\n\n# Multiple questions\nquestions: [{\"id\": \"storage_type\", \"question\": \"Which storage backend?\", \"options\": [{\"label\": \"SQLite\"}, {\"label\": \"PostgreSQL\"}]}, {\"id\": \"auth_method\", \"question\": \"Which auth method?\", \"options\": [{\"label\": \"JWT\"}, {\"label\": \"Session cookies\"}]}]\n</examples>",
287
287
  "parameters": {
288
288
  "type": "object",
289
289
  "properties": {
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2319
2319
  "job": {
2320
2320
  "name": "job",
2321
2321
  "label": "Job",
2322
- "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2322
+ "description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` (`toolInterruptPolicy` is not a gate)). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
2323
2323
  "parameters": {
2324
2324
  "type": "object",
2325
2325
  "properties": {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Utilities for launching an external text editor ($VISUAL / $EDITOR).
3
3
  */
4
+ import { dlopen, FFIType, ptr } from "bun:ffi";
4
5
  import { spawn } from "node:child_process";
5
6
  import * as fs from "node:fs/promises";
6
7
  import * as os from "node:os";
@@ -26,6 +27,85 @@ export function trimEditorTrailingNewline(text: string): string {
26
27
  return text.replace(/\r?\n$/, "");
27
28
  }
28
29
 
30
+ /**
31
+ * Native Windows editor executables must bypass cmd.exe. The shell wrapper
32
+ * reclaims the shared console input buffer while it waits for the editor,
33
+ * reopening the cooked-mode window that loses the first keystroke. Batch
34
+ * files still require a shell on Windows; ordinary executables do not.
35
+ */
36
+ export function shouldUseExternalEditorShell(editor: string, platform: NodeJS.Platform = process.platform): boolean {
37
+ return platform === "win32" && /\.(?:cmd|bat)$/iu.test(editor);
38
+ }
39
+
40
+ /**
41
+ * On Windows the console input buffer is shared between the parent and the
42
+ * child. When the parent TUI stops it restores the console to cooked mode
43
+ * (ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT) and disables
44
+ * ENABLE_VIRTUAL_TERMINAL_INPUT. A child TUI such as Neovim then inherits
45
+ * that cooked state: the very next keystroke is line-buffered until an
46
+ * Enter flushes it — exactly the "first i is swallowed, Enter fixes it"
47
+ * symptom reported in #5224.
48
+ *
49
+ * Neovim (via libuv's uv_tty) re-enables raw VT input on startup, but the
50
+ * race window between the parent's restore and the child's init is enough
51
+ * to deterministically line-buffer the first typed keystroke when the child
52
+ * is spawned through `cmd.exe` (`shell: true`), because `cmd` itself keeps
53
+ * the console in cooked mode while it waits for the child.
54
+ *
55
+ * To make the handover symmetric:
56
+ * - flush any pending console input left over from the parent (e.g. probe
57
+ * reply fragments that were already read into the kernel buffer), and
58
+ * - leave the console in raw VT mode (VT on, LINE/ECHO off) so the child
59
+ * inherits an immediately-usable mode; the caller's `ui.start()` will
60
+ * reconcile the mode again when the editor exits.
61
+ */
62
+ const STD_INPUT_HANDLE = -10;
63
+ const ENABLE_LINE_INPUT = 0x0002;
64
+ const ENABLE_ECHO_INPUT = 0x0004;
65
+ const ENABLE_WINDOW_INPUT = 0x0008;
66
+ const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
67
+
68
+ interface RawModeInput {
69
+ setRawMode?: (mode: boolean) => unknown;
70
+ }
71
+
72
+ export function prepareWindowsConsoleForExternalEditor(
73
+ platform: NodeJS.Platform = process.platform,
74
+ input: RawModeInput = process.stdin,
75
+ ): void {
76
+ if (platform !== "win32") return;
77
+ // Native Windows terminals can expose stdin through a ConPTY pipe rather than
78
+ // a console input handle. In that case GetConsoleMode/SetConsoleMode below
79
+ // cannot re-arm the input stream, but the runtime's TTY implementation can.
80
+ try {
81
+ input.setRawMode?.(true);
82
+ } catch {}
83
+
84
+ try {
85
+ const kernel32 = dlopen("kernel32.dll", {
86
+ GetStdHandle: { args: [FFIType.i32], returns: FFIType.ptr },
87
+ GetConsoleMode: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.bool },
88
+ SetConsoleMode: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.bool },
89
+ FlushConsoleInputBuffer: { args: [FFIType.ptr], returns: FFIType.bool },
90
+ });
91
+ const handle = kernel32.symbols.GetStdHandle(STD_INPUT_HANDLE);
92
+ try {
93
+ kernel32.symbols.FlushConsoleInputBuffer(handle);
94
+ } catch {}
95
+ const mode = new Uint32Array(1);
96
+ const modePtr = ptr(mode);
97
+ if (modePtr && kernel32.symbols.GetConsoleMode(handle, modePtr)) {
98
+ const cur = mode[0]!;
99
+ const next =
100
+ (cur | ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT) & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
101
+ if (next !== cur) {
102
+ kernel32.symbols.SetConsoleMode(handle, next);
103
+ }
104
+ }
105
+ kernel32.close();
106
+ } catch {}
107
+ }
108
+
29
109
  /**
30
110
  * Opens `content` in the user's external editor and returns the edited text.
31
111
  * Returns `null` if the editor exits with a non-zero code.
@@ -46,7 +126,12 @@ export async function openInEditor(
46
126
  const [editor, ...editorArgs] = editorCmd.split(" ");
47
127
  const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
48
128
 
49
- const child = spawn(editor, [...editorArgs, tmpFile], { stdio, shell: process.platform === "win32" });
129
+ prepareWindowsConsoleForExternalEditor();
130
+
131
+ const child = spawn(editor, [...editorArgs, tmpFile], {
132
+ stdio,
133
+ shell: shouldUseExternalEditorShell(editor),
134
+ });
50
135
  const { promise, reject, resolve } = Promise.withResolvers<number>();
51
136
  child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
52
137
  child.once("error", error => reject(error));