@gajae-code/coding-agent 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +41 -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 +8 -2
  35. package/dist/types/sdk/cli/session-cli.d.ts +2 -1
  36. package/dist/types/sdk/host/events.d.ts +1 -0
  37. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  38. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  39. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +18 -17
  41. package/dist/types/sdk/lifecycle-session.d.ts +1 -1
  42. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  43. package/dist/types/sdk/receipt-state.d.ts +2 -0
  44. package/dist/types/sdk/router/session-router.d.ts +4 -0
  45. package/dist/types/sdk/turn-result.d.ts +6 -0
  46. package/dist/types/session/agent-session.d.ts +47 -7
  47. package/dist/types/session/fold-coordinator.d.ts +29 -2
  48. package/dist/types/session/session-manager.d.ts +14 -2
  49. package/dist/types/tools/ask.d.ts +1 -0
  50. package/dist/types/tools/bash.d.ts +12 -0
  51. package/dist/types/tools/browser/launch.d.ts +10 -0
  52. package/dist/types/tools/browser/registry.d.ts +8 -0
  53. package/dist/types/tools/index.d.ts +6 -10
  54. package/dist/types/tools/job.d.ts +3 -1
  55. package/dist/types/utils/external-editor.d.ts +7 -0
  56. package/examples/extensions/README.md +1 -1
  57. package/examples/extensions/pirate.ts +7 -4
  58. package/package.json +7 -7
  59. package/scripts/generate-sdk-operation-inventory.ts +7 -1
  60. package/src/async/job-manager.ts +107 -9
  61. package/src/browser-backend.ts +29 -0
  62. package/src/cli/plugin-cli.ts +29 -0
  63. package/src/cli/update-cli.ts +63 -12
  64. package/src/commands/sdk.ts +22 -7
  65. package/src/config/billing-path.ts +42 -0
  66. package/src/config/config-schema-version.ts +18 -1
  67. package/src/config/model-preset-registry.ts +177 -18
  68. package/src/config/model-registry.ts +268 -42
  69. package/src/config/settings-schema.ts +19 -7
  70. package/src/config/settings.ts +46 -15
  71. package/src/coordinator-mcp/projection-scan.ts +6 -6
  72. package/src/coordinator-mcp/server.ts +21 -1
  73. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  74. package/src/extensibility/extensions/runner.ts +27 -0
  75. package/src/extensibility/extensions/types.ts +8 -3
  76. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  77. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  78. package/src/extensibility/skills.ts +22 -12
  79. package/src/gjc-runtime/launch-worktree.ts +285 -4
  80. package/src/gjc-runtime/session-state-lock.ts +154 -28
  81. package/src/gjc-runtime/session-state-sidecar.ts +10 -17
  82. package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
  83. package/src/internal-urls/docs-index.generated.ts +10 -10
  84. package/src/main.ts +4 -1
  85. package/src/modes/acp/acp-agent.ts +5 -5
  86. package/src/modes/components/model-selector.ts +76 -8
  87. package/src/modes/components/session-selector.ts +23 -15
  88. package/src/modes/components/sessions-dashboard.ts +8 -5
  89. package/src/modes/controllers/command-controller.ts +1 -1
  90. package/src/modes/controllers/event-controller.ts +30 -4
  91. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  92. package/src/modes/controllers/input-controller.ts +2 -2
  93. package/src/modes/controllers/selector-controller.ts +10 -3
  94. package/src/modes/interactive-mode.ts +36 -12
  95. package/src/modes/prompt-action-autocomplete.ts +30 -4
  96. package/src/modes/runtime-init.ts +12 -9
  97. package/src/modes/theme/theme.ts +5 -0
  98. package/src/modes/types.ts +2 -0
  99. package/src/modes/utils/ui-helpers.ts +22 -3
  100. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  101. package/src/prompts/tools/ask.md +1 -1
  102. package/src/prompts/tools/bash.md +1 -0
  103. package/src/prompts/tools/job.md +1 -1
  104. package/src/sdk/acp/adapter.ts +5 -0
  105. package/src/sdk/broker/broker.ts +10 -1
  106. package/src/sdk/broker/ensure.ts +76 -3
  107. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  108. package/src/sdk/broker/lifecycle.ts +254 -34
  109. package/src/sdk/broker/spawn-substrate.ts +45 -2
  110. package/src/sdk/broker/startup-budget.ts +144 -2
  111. package/src/sdk/broker/state-version.ts +17 -0
  112. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  113. package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
  114. package/src/sdk/bus/discord-daemon.ts +50 -10
  115. package/src/sdk/bus/index.ts +33 -3
  116. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  117. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  118. package/src/sdk/bus/notification-service.ts +22 -5
  119. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  120. package/src/sdk/bus/reconciliation-store.ts +55 -31
  121. package/src/sdk/bus/slack-conversation.ts +1 -1
  122. package/src/sdk/bus/slack-daemon.ts +72 -38
  123. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  124. package/src/sdk/cli/rows.ts +30 -7
  125. package/src/sdk/cli/session-cli.ts +33 -21
  126. package/src/sdk/host/control/dispatch.ts +2 -2
  127. package/src/sdk/host/events.ts +1 -0
  128. package/src/sdk/host/host.ts +2 -0
  129. package/src/sdk/host/query/cursor.ts +52 -7
  130. package/src/sdk/host/query/handlers.ts +33 -12
  131. package/src/sdk/host/query/revision-store.ts +12 -0
  132. package/src/sdk/host/session-runtime.test.ts +463 -8
  133. package/src/sdk/host/session-runtime.ts +383 -54
  134. package/src/sdk/host/surface-policy.ts +3 -3
  135. package/src/sdk/lifecycle-session.ts +7 -2
  136. package/src/sdk/prompt-deadline-manager.ts +2 -1
  137. package/src/sdk/protocol/operation-inventory.generated.json +54 -3
  138. package/src/sdk/protocol/operation-registry.ts +1 -1
  139. package/src/sdk/receipt-state.ts +10 -0
  140. package/src/sdk/router/session-router.ts +152 -59
  141. package/src/sdk/session.ts +60 -41
  142. package/src/sdk/turn-result.ts +22 -0
  143. package/src/session/agent-session.ts +1033 -260
  144. package/src/session/fold-coordinator.ts +40 -3
  145. package/src/session/session-manager.ts +517 -77
  146. package/src/slash-commands/builtin-registry.ts +41 -0
  147. package/src/task/executor.ts +9 -4
  148. package/src/tools/ask-contract.test.ts +64 -0
  149. package/src/tools/ask-contract.ts +36 -1
  150. package/src/tools/ask.ts +10 -8
  151. package/src/tools/bash-interactive.ts +11 -2
  152. package/src/tools/bash.ts +156 -33
  153. package/src/tools/browser/launch.ts +37 -0
  154. package/src/tools/browser/registry.ts +99 -9
  155. package/src/tools/cron.ts +6 -4
  156. package/src/tools/descriptors.test.ts +15 -0
  157. package/src/tools/descriptors.ts +5 -1
  158. package/src/tools/index.ts +6 -6
  159. package/src/tools/job.ts +4 -1
  160. package/src/tools/monitor.ts +12 -9
  161. package/src/tools/resolve.ts +30 -10
  162. package/src/tools/tool-catalog.generated.ts +2 -2
  163. package/src/utils/external-editor.ts +72 -1
@@ -139,6 +139,29 @@ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRun
139
139
  };
140
140
  }
141
141
 
142
+ async function updateSessionStar(
143
+ starred: boolean,
144
+ command: ParsedSlashCommand,
145
+ runtime: SlashCommandRuntime,
146
+ ): Promise<SlashCommandResult> {
147
+ if (command.args) return usage(`Usage: /${starred ? "star" : "unstar"}`, runtime);
148
+ if (!runtime.sessionManager.isPersisted()) {
149
+ await runtime.output("Session stars require a persisted session.");
150
+ return commandConsumed();
151
+ }
152
+ const changed = await runtime.sessionManager.setSessionStarred(starred);
153
+ await runtime.output(
154
+ changed
155
+ ? starred
156
+ ? "Session starred."
157
+ : "Session unstarred."
158
+ : starred
159
+ ? "Session is already starred."
160
+ : "Session is not starred.",
161
+ );
162
+ return commandConsumed();
163
+ }
164
+
142
165
  function parseProviderSetupSlashArgs(args: string): {
143
166
  preset?: string;
144
167
  compat?: string;
@@ -1961,6 +1984,24 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1961
1984
  await runtime.ctx.handleContributionPrepCommand(command.args || undefined);
1962
1985
  },
1963
1986
  },
1987
+ {
1988
+ name: "star",
1989
+ priority: 91,
1990
+ description: "Star the current session for easier discovery",
1991
+ // Accept args at dispatch so the handler can consume them as a usage error
1992
+ // instead of silently forwarding `/star ...` to the model.
1993
+ allowArgs: true,
1994
+ handle: (command, runtime) => updateSessionStar(true, command, runtime),
1995
+ },
1996
+ {
1997
+ name: "unstar",
1998
+ priority: 91,
1999
+ description: "Remove the star from the current session",
2000
+ // Accept args at dispatch so the handler can consume them as a usage error
2001
+ // instead of silently forwarding `/unstar ...` to the model.
2002
+ allowArgs: true,
2003
+ handle: (command, runtime) => updateSessionStar(false, command, runtime),
2004
+ },
1964
2005
  {
1965
2006
  name: "resume",
1966
2007
  priority: 92,
@@ -2114,12 +2114,17 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
2114
2114
  if (extensionRunner && !options.preflightProbe) {
2115
2115
  extensionRunner.initialize(
2116
2116
  {
2117
+ // Extension sends are INDEPENDENT producers (see runtime-init):
2118
+ // classify them at the trusted adapter so a terminal abort keeps
2119
+ // them rather than dropping them as turn-owned continuations.
2117
2120
  sendMessage: (message, options) => {
2118
- const sendPromise = session.sendCustomMessage(message, options).catch(e => {
2119
- logger.error("Extension sendMessage failed", {
2120
- error: e instanceof Error ? e.message : String(e),
2121
+ const sendPromise = session
2122
+ .sendCustomMessage(message, { ...options, origin: "external" })
2123
+ .catch(e => {
2124
+ logger.error("Extension sendMessage failed", {
2125
+ error: e instanceof Error ? e.message : String(e),
2126
+ });
2121
2127
  });
2122
- });
2123
2128
  pendingExtensionMessages.push(sendPromise);
2124
2129
  },
2125
2130
  sendUserMessage: (content, options) => {
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validRecommendedIndex } from "./ask";
3
+ import { askSchema, ordinaryAskSchema } from "./ask-contract";
4
+
5
+ const baseQuestion = {
6
+ id: "q",
7
+ question: "Choose one.",
8
+ options: [{ label: "Keep current behavior" }, { label: "Use repository default" }],
9
+ };
10
+
11
+ describe("ask option contract", () => {
12
+ test("rejects custom-input pseudo-options", () => {
13
+ for (const label of [
14
+ "Other (type your own)",
15
+ "Type your own",
16
+ "custom input",
17
+ "직접 입력",
18
+ "1. 직접 입력",
19
+ "직접 입력으로 추가",
20
+ " OTHER(SPECIFY) ",
21
+ "other…",
22
+ "write your own",
23
+ "enter manually",
24
+ "기타",
25
+ "기타 입력",
26
+ ]) {
27
+ const result = ordinaryAskSchema.safeParse({
28
+ questions: [{ ...baseQuestion, options: [{ label }, { label: "Cancel" }], recommended: 0 }],
29
+ });
30
+ expect(result.success, label).toBe(false);
31
+ }
32
+ });
33
+
34
+ test("accepts a genuine Other domain value", () => {
35
+ const result = ordinaryAskSchema.safeParse({
36
+ questions: [{ ...baseQuestion, options: [{ label: "Other" }, { label: "Cancel" }] }],
37
+ });
38
+ expect(result.success).toBe(true);
39
+ });
40
+
41
+ test("accepts descriptive labels that merely contain custom-input words", () => {
42
+ const result = ordinaryAskSchema.safeParse({
43
+ questions: [{ ...baseQuestion, options: [{ label: "Deploy Other (type your own)" }, { label: "Cancel" }] }],
44
+ });
45
+ expect(result.success).toBe(true);
46
+ });
47
+
48
+ test("accepts ordinary recommended options", () => {
49
+ const result = ordinaryAskSchema.safeParse({ questions: [{ ...baseQuestion, recommended: 0 }] });
50
+ expect(result.success).toBe(true);
51
+ });
52
+
53
+ test("applies pseudo-option rejection to deep-interview ask schema", () => {
54
+ const result = askSchema.safeParse({
55
+ questions: [{ ...baseQuestion, options: [{ label: "직접 입력으로 추가" }, { label: "취소" }] }],
56
+ });
57
+ expect(result.success).toBe(false);
58
+ });
59
+
60
+ test("does not recommend single-option questions", () => {
61
+ expect(validRecommendedIndex(0, 1)).toBeUndefined();
62
+ expect(validRecommendedIndex(0, 2)).toBe(0);
63
+ });
64
+ });
@@ -26,8 +26,43 @@ function deepInterviewBoundedString(maximum: number) {
26
26
  });
27
27
  }
28
28
 
29
+ function isReservedCustomInputOptionLabel(label: string): boolean {
30
+ const withoutNumbering = label
31
+ .normalize("NFKC")
32
+ .toLowerCase()
33
+ .trim()
34
+ .replace(/^\d+[.)]\s+/u, "");
35
+ if (/^other[\p{P}\p{S}\s]*$/u.test(withoutNumbering) && withoutNumbering !== "other") return true;
36
+ const wordsOnly = withoutNumbering
37
+ .replace(/[\p{P}\p{S}]+/gu, " ")
38
+ .replace(/\s+/g, " ")
39
+ .trim();
40
+ const englishPseudoOptions = new Set([
41
+ "other type your own",
42
+ "other specify",
43
+ "other please specify",
44
+ "type your own",
45
+ "write your own",
46
+ "enter manually",
47
+ "custom input",
48
+ ]);
49
+ if (englishPseudoOptions.has(wordsOnly)) return true;
50
+ const compact = wordsOnly.replace(/\s+/g, "");
51
+ return ["직접입력", "직접입력으로추가", "기타", "기타입력"].includes(compact);
52
+ }
53
+
29
54
  const OptionItem = z.object({
30
- label: z.string().describe("display label"),
55
+ label: z
56
+ .string()
57
+ .describe("display label")
58
+ .superRefine((value, context) => {
59
+ if (isReservedCustomInputOptionLabel(value))
60
+ context.addIssue({
61
+ code: "custom",
62
+ message:
63
+ "ask option labels must not imitate the reserved custom-input control; omit the pseudo-option and let the Ask UI add Other (type your own)",
64
+ });
65
+ }),
31
66
  });
32
67
 
33
68
  const DEEP_INTERVIEW_INTENT_ID_PATTERN = /^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/;
package/src/tools/ask.ts CHANGED
@@ -158,7 +158,8 @@ function getDoneOptionLabel(): string {
158
158
  return success ? `${success} Done selecting` : "Done selecting";
159
159
  }
160
160
 
161
- function validRecommendedIndex(recommended: number | undefined, optionCount: number): number | undefined {
161
+ export function validRecommendedIndex(recommended: number | undefined, optionCount: number): number | undefined {
162
+ if (optionCount <= 1) return undefined;
162
163
  return typeof recommended === "number" &&
163
164
  Number.isFinite(recommended) &&
164
165
  Number.isInteger(recommended) &&
@@ -350,7 +351,7 @@ async function askSingleQuestion(
350
351
  options: AskSingleQuestionOptions = {},
351
352
  ): Promise<SelectionResult> {
352
353
  const {
353
- recommended,
354
+ recommended: requestedRecommended,
354
355
  timeout,
355
356
  signal,
356
357
  initialSelection,
@@ -358,6 +359,7 @@ async function askSingleQuestion(
358
359
  scrollTitleRows,
359
360
  autoSelectOnTimeout = true,
360
361
  } = options;
362
+ const recommended = validRecommendedIndex(requestedRecommended, optionLabels.length);
361
363
  const doneLabel = getDoneOptionLabel();
362
364
  const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
363
365
  const clarificationOptionLabel = options.clarificationOptionLabel;
@@ -1118,6 +1120,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1118
1120
  },
1119
1121
  ) => {
1120
1122
  const rawOptionLabels = q.options.map(o => o.label);
1123
+ const recommended = validRecommendedIndex(q.recommended, rawOptionLabels.length);
1121
1124
  const questionIndex = params.questions.indexOf(q);
1122
1125
  const emptyCustomAttempt = options?.emptyCustomAttempt ?? 0;
1123
1126
  // Route headless asks through the SDK workflow-gate emitter; a connected
@@ -1128,7 +1131,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1128
1131
  question: q.question,
1129
1132
  options: q.options,
1130
1133
  multi: q.multi,
1131
- recommended: q.recommended,
1134
+ recommended,
1132
1135
  deepInterview: q.deepInterview,
1133
1136
  workflowGate: q.workflowGate,
1134
1137
  allowEmpty: q.multi === true && params.questions.length > 1,
@@ -1177,12 +1180,11 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1177
1180
  }),
1178
1181
  }
1179
1182
  : options?.previous;
1180
- const recommendedIndex = validRecommendedIndex(q.recommended, rawOptionLabels.length);
1181
1183
  activeRemoteRequest = {
1182
1184
  question: displayQuestion,
1183
1185
  options: remoteSelectorOptions,
1184
1186
  interaction: "selector",
1185
- ...(recommendedIndex === undefined ? {} : { recommendedIndex }),
1187
+ ...(recommended === undefined ? {} : { recommendedIndex: recommended }),
1186
1188
  ...(timeout === undefined || timeout === null ? {} : { timeoutMs: timeout }),
1187
1189
  ...(clarificationOptionLabel ? { transitionCount: 2 } : { transitionCount: 1 }),
1188
1190
  multi: q.multi === true,
@@ -1204,7 +1206,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1204
1206
  cancelled,
1205
1207
  timedOut,
1206
1208
  } = await askSingleQuestion(ui, displayQuestion, optionLabels, q.multi ?? false, {
1207
- recommended: q.recommended,
1209
+ recommended,
1208
1210
  timeout: timeout ?? undefined,
1209
1211
  signal,
1210
1212
  initialSelection,
@@ -1221,8 +1223,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1221
1223
  question: displayQuestion,
1222
1224
  options: state.interaction === "selector" ? remoteSelectorOptions : [],
1223
1225
  interaction: state.interaction,
1224
- ...(state.interaction === "selector" && recommendedIndex !== undefined
1225
- ? { recommendedIndex }
1226
+ ...(state.interaction === "selector" && recommended !== undefined
1227
+ ? { recommendedIndex: recommended }
1226
1228
  : {}),
1227
1229
  ...(state.interaction === "selector" && timeout !== undefined && timeout !== null
1228
1230
  ? { timeoutMs: timeout }
@@ -377,6 +377,7 @@ export async function runInteractiveBashPty(
377
377
  let observer: BashInteractiveOverlayComponent | undefined;
378
378
  let settleForeground: ((result: BashInteractiveResult) => void) | undefined;
379
379
  let settled = false;
380
+ let inputAttached = true;
380
381
  const foreground = Promise.withResolvers<BashInteractiveResult>();
381
382
  const terminal = Promise.withResolvers<BashInteractiveResult>();
382
383
 
@@ -449,6 +450,10 @@ export async function runInteractiveBashPty(
449
450
  options.signal?.removeEventListener("abort", forwardForegroundAbort);
450
451
  },
451
452
  detachObserver: (foldResult: BashInteractiveResult) => {
453
+ // Stop accepting bytes synchronously at the ownership-transfer boundary.
454
+ // Overlay teardown may be deferred by the host, so disposal alone is not
455
+ // a sufficient input fence for a folded, output-only PTY.
456
+ inputAttached = false;
452
457
  const outcome = settle(foldResult);
453
458
  if (outcome === "resolved") observer = undefined;
454
459
  return outcome;
@@ -476,15 +481,19 @@ export async function runInteractiveBashPty(
476
481
  // Escape explicitly force-kills, while disposal never kills the work.
477
482
  component.setHandlers(
478
483
  data => {
484
+ if (!inputAttached) return;
479
485
  try {
480
486
  session.write(data);
481
487
  } catch {
482
488
  // ignore writes after the command exits
483
489
  }
484
490
  },
485
- options.onDismiss ?? (() => session.kill()),
491
+ () => {
492
+ if (!inputAttached) return;
493
+ (options.onDismiss ?? (() => session.kill()))();
494
+ },
486
495
  () => {},
487
- options.onFoldKey ?? (() => false),
496
+ () => inputAttached && (options.onFoldKey?.() ?? false),
488
497
  );
489
498
  observer = component;
490
499
  return component;