@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.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 (298) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +69 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +11 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.js +3 -0
  7. package/dist/agent-whats-new.js.map +1 -1
  8. package/dist/antigravity-usage-scan.d.ts +58 -0
  9. package/dist/antigravity-usage-scan.js +257 -0
  10. package/dist/antigravity-usage-scan.js.map +1 -0
  11. package/dist/antigravity-usage-worker.d.ts +1 -0
  12. package/dist/antigravity-usage-worker.js +10 -0
  13. package/dist/antigravity-usage-worker.js.map +1 -0
  14. package/dist/auth-api.js +4 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/kiro-classic-archive.d.ts +52 -0
  17. package/dist/backend/kiro-classic-archive.js +444 -0
  18. package/dist/backend/kiro-classic-archive.js.map +1 -0
  19. package/dist/backend/kiro-identity.d.ts +83 -0
  20. package/dist/backend/kiro-identity.js +144 -0
  21. package/dist/backend/kiro-identity.js.map +1 -1
  22. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  23. package/dist/backend/kiro-migration-verify.js +52 -0
  24. package/dist/backend/kiro-migration-verify.js.map +1 -0
  25. package/dist/backend/kiro-retired-model.d.ts +62 -0
  26. package/dist/backend/kiro-retired-model.js +160 -0
  27. package/dist/backend/kiro-retired-model.js.map +1 -0
  28. package/dist/backend/kiro-v2-store-worker.js +21 -1
  29. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  30. package/dist/backend/kiro-v2-store.d.ts +8 -0
  31. package/dist/backend/kiro-v2-store.js.map +1 -1
  32. package/dist/backend/kiro.d.ts +169 -2
  33. package/dist/backend/kiro.js +703 -28
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/types.d.ts +108 -0
  36. package/dist/backend/types.js +9 -0
  37. package/dist/backend/types.js.map +1 -1
  38. package/dist/backend-signin-guide.d.ts +1 -0
  39. package/dist/backend-signin-guide.js +9 -0
  40. package/dist/backend-signin-guide.js.map +1 -0
  41. package/dist/backend-signin-pending.d.ts +10 -0
  42. package/dist/backend-signin-pending.js +44 -0
  43. package/dist/backend-signin-pending.js.map +1 -0
  44. package/dist/browser-backend-signins.d.ts +56 -0
  45. package/dist/browser-backend-signins.js +189 -0
  46. package/dist/browser-backend-signins.js.map +1 -0
  47. package/dist/cache-ledger.d.ts +38 -2
  48. package/dist/cache-ledger.js +133 -8
  49. package/dist/cache-ledger.js.map +1 -1
  50. package/dist/cache-prices.d.ts +2 -1
  51. package/dist/cache-prices.js +5 -2
  52. package/dist/cache-prices.js.map +1 -1
  53. package/dist/cache-service.d.ts +68 -0
  54. package/dist/cache-service.js +355 -13
  55. package/dist/cache-service.js.map +1 -1
  56. package/dist/channel/adapters/discord.d.ts +11 -0
  57. package/dist/channel/adapters/discord.js +71 -14
  58. package/dist/channel/adapters/discord.js.map +1 -1
  59. package/dist/channel/ipc-timeouts.js +3 -0
  60. package/dist/channel/ipc-timeouts.js.map +1 -1
  61. package/dist/channel/mcp-tools.js +41 -2
  62. package/dist/channel/mcp-tools.js.map +1 -1
  63. package/dist/channel/tool-router.js +2 -1
  64. package/dist/channel/tool-router.js.map +1 -1
  65. package/dist/classic-channel-manager.d.ts +11 -0
  66. package/dist/classic-channel-manager.js +39 -1
  67. package/dist/classic-channel-manager.js.map +1 -1
  68. package/dist/cli.js +54 -2
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config-validator.d.ts +7 -1
  71. package/dist/config-validator.js +55 -10
  72. package/dist/config-validator.js.map +1 -1
  73. package/dist/config.d.ts +19 -0
  74. package/dist/config.js +33 -1
  75. package/dist/config.js.map +1 -1
  76. package/dist/cpu-quota.d.ts +23 -0
  77. package/dist/cpu-quota.js +86 -0
  78. package/dist/cpu-quota.js.map +1 -0
  79. package/dist/daemon.d.ts +68 -0
  80. package/dist/daemon.js +448 -26
  81. package/dist/daemon.js.map +1 -1
  82. package/dist/dashboard-permissions.d.ts +31 -6
  83. package/dist/dashboard-permissions.js +84 -9
  84. package/dist/dashboard-permissions.js.map +1 -1
  85. package/dist/doctor.js +2 -1
  86. package/dist/doctor.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +1 -1
  88. package/dist/fleet-control-audit.js +3 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-manager.d.ts +133 -2
  91. package/dist/fleet-manager.js +905 -94
  92. package/dist/fleet-manager.js.map +1 -1
  93. package/dist/forged-envelope-scan.d.ts +9 -0
  94. package/dist/forged-envelope-scan.js +30 -5
  95. package/dist/forged-envelope-scan.js.map +1 -1
  96. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  97. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +24 -1
  98. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
  99. package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
  100. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
  101. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
  102. package/dist/instance-config-impact.js +10 -5
  103. package/dist/instance-config-impact.js.map +1 -1
  104. package/dist/instance-lifecycle.d.ts +23 -0
  105. package/dist/instance-lifecycle.js +62 -12
  106. package/dist/instance-lifecycle.js.map +1 -1
  107. package/dist/kiro-db-reader.d.ts +5 -0
  108. package/dist/kiro-db-reader.js +18 -0
  109. package/dist/kiro-db-reader.js.map +1 -1
  110. package/dist/kiro-engine-status.d.ts +2 -0
  111. package/dist/kiro-engine-status.js +9 -0
  112. package/dist/kiro-engine-status.js.map +1 -1
  113. package/dist/kiro-migrate.d.ts +60 -0
  114. package/dist/kiro-migrate.js +160 -0
  115. package/dist/kiro-migrate.js.map +1 -0
  116. package/dist/kiro-session-proof.d.ts +41 -0
  117. package/dist/kiro-session-proof.js +89 -0
  118. package/dist/kiro-session-proof.js.map +1 -0
  119. package/dist/kiro-transcript-lane.d.ts +2 -0
  120. package/dist/kiro-transcript-lane.js.map +1 -1
  121. package/dist/kiro-transcript-worker.js +1 -0
  122. package/dist/kiro-transcript-worker.js.map +1 -1
  123. package/dist/kiro-tui-forensic.d.ts +12 -0
  124. package/dist/kiro-tui-forensic.js +110 -0
  125. package/dist/kiro-tui-forensic.js.map +1 -0
  126. package/dist/kiro-usage-run.d.ts +7 -0
  127. package/dist/kiro-usage-run.js +39 -0
  128. package/dist/kiro-usage-run.js.map +1 -0
  129. package/dist/kiro-usage-scan.d.ts +49 -0
  130. package/dist/kiro-usage-scan.js +223 -0
  131. package/dist/kiro-usage-scan.js.map +1 -0
  132. package/dist/kiro-usage-worker.d.ts +1 -0
  133. package/dist/kiro-usage-worker.js +10 -0
  134. package/dist/kiro-usage-worker.js.map +1 -0
  135. package/dist/kiro-usage.d.ts +58 -0
  136. package/dist/kiro-usage.js +310 -0
  137. package/dist/kiro-usage.js.map +1 -0
  138. package/dist/locale.js +230 -8
  139. package/dist/locale.js.map +1 -1
  140. package/dist/log-service.js +1 -1
  141. package/dist/log-service.js.map +1 -1
  142. package/dist/login-url.d.ts +4 -0
  143. package/dist/login-url.js +22 -0
  144. package/dist/login-url.js.map +1 -0
  145. package/dist/needs-you-hub.d.ts +2 -1
  146. package/dist/needs-you-hub.js +1 -1
  147. package/dist/needs-you-hub.js.map +1 -1
  148. package/dist/needs-you.d.ts +10 -2
  149. package/dist/needs-you.js +10 -2
  150. package/dist/needs-you.js.map +1 -1
  151. package/dist/notice-delivery.d.ts +16 -0
  152. package/dist/notice-delivery.js +35 -0
  153. package/dist/notice-delivery.js.map +1 -0
  154. package/dist/office-share-page.d.ts +16 -0
  155. package/dist/office-share-page.js +85 -0
  156. package/dist/office-share-page.js.map +1 -0
  157. package/dist/office-share-service.d.ts +91 -0
  158. package/dist/office-share-service.js +176 -0
  159. package/dist/office-share-service.js.map +1 -0
  160. package/dist/office-share.d.ts +268 -0
  161. package/dist/office-share.js +574 -0
  162. package/dist/office-share.js.map +1 -0
  163. package/dist/opencode-usage-scan.d.ts +28 -0
  164. package/dist/opencode-usage-scan.js +92 -0
  165. package/dist/opencode-usage-scan.js.map +1 -0
  166. package/dist/opencode-usage-worker.d.ts +1 -0
  167. package/dist/opencode-usage-worker.js +10 -0
  168. package/dist/opencode-usage-worker.js.map +1 -0
  169. package/dist/outbound-handlers.js +22 -2
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +65 -2
  172. package/dist/outbound-schemas.js +56 -2
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-web-gateway.js +7 -1
  175. package/dist/public-web-gateway.js.map +1 -1
  176. package/dist/quickstart-api.js +13 -2
  177. package/dist/quickstart-api.js.map +1 -1
  178. package/dist/quickstart.js +2 -1
  179. package/dist/quickstart.js.map +1 -1
  180. package/dist/remote-card-answer.d.ts +83 -0
  181. package/dist/remote-card-answer.js +208 -0
  182. package/dist/remote-card-answer.js.map +1 -0
  183. package/dist/remote-card-prompt.d.ts +38 -0
  184. package/dist/remote-card-prompt.js +86 -0
  185. package/dist/remote-card-prompt.js.map +1 -0
  186. package/dist/reply-triggers.d.ts +2 -0
  187. package/dist/reply-triggers.js +3 -0
  188. package/dist/reply-triggers.js.map +1 -0
  189. package/dist/settings-api.d.ts +2 -0
  190. package/dist/settings-api.js +41 -16
  191. package/dist/settings-api.js.map +1 -1
  192. package/dist/settings-change.js +20 -14
  193. package/dist/settings-change.js.map +1 -1
  194. package/dist/settings-effect.js +7 -1
  195. package/dist/settings-effect.js.map +1 -1
  196. package/dist/settings-instance-patch.js +4 -1
  197. package/dist/settings-instance-patch.js.map +1 -1
  198. package/dist/setup-form.d.ts +1 -1
  199. package/dist/setup-form.js +55 -6
  200. package/dist/setup-form.js.map +1 -1
  201. package/dist/setup-host.d.ts +9 -0
  202. package/dist/setup-host.js +127 -6
  203. package/dist/setup-host.js.map +1 -1
  204. package/dist/setup-signin-assets.d.ts +2 -0
  205. package/dist/setup-signin-assets.js +6 -0
  206. package/dist/setup-signin-assets.js.map +1 -0
  207. package/dist/setup-wizard.js +2 -0
  208. package/dist/setup-wizard.js.map +1 -1
  209. package/dist/structured-signin-transport.d.ts +8 -0
  210. package/dist/structured-signin-transport.js +79 -0
  211. package/dist/structured-signin-transport.js.map +1 -0
  212. package/dist/structured-signin.d.ts +131 -0
  213. package/dist/structured-signin.js +388 -0
  214. package/dist/structured-signin.js.map +1 -0
  215. package/dist/tool-permissions.js +29 -0
  216. package/dist/tool-permissions.js.map +1 -1
  217. package/dist/topic-commands.js +6 -2
  218. package/dist/topic-commands.js.map +1 -1
  219. package/dist/transcript-sources.d.ts +27 -2
  220. package/dist/transcript-sources.js +104 -40
  221. package/dist/transcript-sources.js.map +1 -1
  222. package/dist/turn-reply-guard.d.ts +1 -1
  223. package/dist/turn-reply-guard.js +1 -1
  224. package/dist/turn-reply-guard.js.map +1 -1
  225. package/dist/types.d.ts +33 -8
  226. package/dist/ui/backend-signin.js +63 -10
  227. package/dist/ui/chat-store.js +3 -2
  228. package/dist/ui/chat-strings.js +4 -4
  229. package/dist/ui/fleet-office-editor.js +68 -11
  230. package/dist/ui/fleet-office-layout.js +110 -35
  231. package/dist/ui/fleet-office-model.js +6 -3
  232. package/dist/ui/fleet-office-overview.js +167 -0
  233. package/dist/ui/fleet-office-pack.d.ts +13 -3
  234. package/dist/ui/fleet-office-pack.js +214 -24
  235. package/dist/ui/fleet-office-skin.js +98 -23
  236. package/dist/ui/fleet-office-store.js +82 -5
  237. package/dist/ui/fleet-office-view.js +5 -4
  238. package/dist/ui/fleet-office-zones.js +232 -21
  239. package/dist/ui/fleet-office.js +549 -186
  240. package/dist/ui/fleet-usage.js +19 -15
  241. package/dist/ui/office-share-dialog.js +145 -0
  242. package/dist/ui/office-share-page.js +164 -0
  243. package/dist/ui/panel-chat.js +22 -3
  244. package/dist/ui/panel-log.js +4 -4
  245. package/dist/ui/panel-needs.js +5 -5
  246. package/dist/ui/panel-settings.js +38 -12
  247. package/dist/ui/settings-dialogs.js +8 -1
  248. package/dist/ui/settings-guests.js +17 -7
  249. package/dist/ui/settings-model.js +22 -6
  250. package/dist/ui/settings-registry.js +4 -0
  251. package/dist/ui/settings-strings.js +18 -2
  252. package/dist/ui/shared/app-i18n.js +4 -2
  253. package/dist/ui/shared/app-shell.js +14 -6
  254. package/dist/ui/shared/app-stream.js +2 -1
  255. package/dist/ui/shared/app.css +85 -4
  256. package/dist/ui/shared/office/cozy.png +0 -0
  257. package/dist/ui/shared/side-panel.js +95 -0
  258. package/dist/ui/structured-signin.js +115 -0
  259. package/dist/usage-report.d.ts +20 -0
  260. package/dist/usage-report.js +54 -17
  261. package/dist/usage-report.js.map +1 -1
  262. package/dist/usage-rollup.d.ts +9 -1
  263. package/dist/usage-rollup.js +20 -4
  264. package/dist/usage-rollup.js.map +1 -1
  265. package/dist/web-api.d.ts +23 -1
  266. package/dist/web-api.js +292 -3
  267. package/dist/web-api.js.map +1 -1
  268. package/dist/web-auth.js +1 -1
  269. package/dist/web-auth.js.map +1 -1
  270. package/dist/web-backend-login.d.ts +1 -7
  271. package/dist/web-backend-login.js +2 -24
  272. package/dist/web-backend-login.js.map +1 -1
  273. package/dist/web-office-acts.d.ts +173 -0
  274. package/dist/web-office-acts.js +285 -0
  275. package/dist/web-office-acts.js.map +1 -0
  276. package/dist/web-office-pack-read.d.ts +35 -0
  277. package/dist/web-office-pack-read.js +252 -0
  278. package/dist/web-office-pack-read.js.map +1 -0
  279. package/dist/web-office-pack-tools.d.ts +232 -0
  280. package/dist/web-office-pack-tools.js +754 -0
  281. package/dist/web-office-pack-tools.js.map +1 -0
  282. package/dist/web-office-skins.d.ts +71 -6
  283. package/dist/web-office-skins.js +321 -35
  284. package/dist/web-office-skins.js.map +1 -1
  285. package/dist/web-office-store.d.ts +5 -2
  286. package/dist/web-office-store.js +69 -10
  287. package/dist/web-office-store.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +4 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/working-recovery.d.ts +2 -2
  292. package/dist/working-recovery.js +43 -8
  293. package/dist/working-recovery.js.map +1 -1
  294. package/dist/working-state-journal.d.ts +2 -0
  295. package/dist/working-state-journal.js +16 -1
  296. package/dist/working-state-journal.js.map +1 -1
  297. package/package.json +3 -2
  298. package/templates/windows-shortcuts.ps1 +155 -140
@@ -5,14 +5,17 @@ import { homedir } from "node:os";
5
5
  import { getAgendHome } from "../paths.js";
6
6
  import { credentialHomeSpec, credentialProfileHome, credentialProfileStoreHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
7
7
  import { kiroAgentName, kiroFleetTag, writeKiroAgent, removeKiroAgent, writeSharedKiroMcpEntries, removeSharedKiroMcpEntries, writeTaggedKiroSteering, removeTaggedKiroSteering, } from "./kiro-agent.js";
8
- import { resolveKiroIdentity, listKiroV1Sessions, listKiroV2Sessions, confirmKiroAgentSwitch, kiroAgentConfirmed, forgetKiroIdentity, kiroIdentityNeedsStore, kiroCanonicalDirectory, KiroIdentityError, } from "./kiro-identity.js";
8
+ import { resolveKiroIdentity, listKiroV1Sessions, listKiroV2Sessions, confirmKiroAgentSwitch, kiroAgentConfirmed, forgetKiroIdentity, kiroIdentityNeedsStore, kiroRecordedConversation, kiroCanonicalDirectory, KiroIdentityError, kiroEngineFloor, kiroCarryCandidate, } from "./kiro-identity.js";
9
9
  import { readKiroLedger, kiroHasNoLaunchRecord } from "./kiro-engine-ledger.js";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { writeFileSync, chmodSync, statSync } from "node:fs";
12
- import { UnsupportedCliError, resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
12
+ import { UnsupportedCliError, ResumeBlockedError, resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
13
+ import { KIRO_SESSION_ID_COMMAND, kiroTitleCommand } from "../kiro-session-proof.js";
13
14
  import { PIE_CLASS } from "../tui-glyphs.js";
14
15
  import { KIRO_EXPIRED_LOGIN_SCREEN } from "../login-flows.js";
15
16
  import { t } from "../locale.js";
17
+ import { sharedKiroV2StoreLane } from "./kiro-v2-store.js";
18
+ import { decideKiroModelError, readKiroKnownModels, readKiroModelRecords, readKiroV1SavedModel, readKiroV2SavedModel, writeKiroModelRecords, KIRO_FALLBACK_MODEL } from "./kiro-retired-model.js";
16
19
  import { recordKiroLaunch } from "./kiro-engine-ledger.js";
17
20
  import { forgetKiroV3Identity, resolveKiroV3Resume } from "./kiro-v3-identity.js";
18
21
  // Kiro CLI feature gates. These are deliberately separate: the flags shipped
@@ -40,7 +43,7 @@ export const KIRO_INSTANCE_AGENT_MIN = "2.21.0";
40
43
  * the binary's own --help instead of the version table, and the operator is
41
44
  * told the version is unverified.
42
45
  */
43
- export const KIRO_TESTED_MAX = "2.33.0";
46
+ export const KIRO_TESTED_MAX = "2.34.0";
44
47
  /**
45
48
  * kiro-cli versions, per TUI front-end, whose mid-turn steering AgEnD drives (#1405). Only kiro's TUI steers — typed
46
49
  * input while a turn runs is injected into it ("steer", the default) or held for its end ("queue", Ctrl+S toggles); the
@@ -48,10 +51,13 @@ export const KIRO_TESTED_MAX = "2.33.0";
48
51
  * queued message mid-tool — has been captured (tests/fixtures/kiro-steer-1405, 2026-10-08: 2.27.1 `--tui
49
52
  * --agent-engine=v2` and `--v3`, 2.28.0 `--tui --agent-engine=v2`; identical rows on all three). Any other version,
50
53
  * 2.28.0's v3, the legacy UI and an undetected version all deliver a steer as an ordinary message after the turn.
54
+ * 2.34.0 TUI was checked on the user-authorized #2598 D5 run (tests/fixtures/kiro-2.34.0-tui-d5):
55
+ * native steer during a read-only tool, a fleet message queued mid-tool, and a native queue entry in the busy turn.
51
56
  */
52
57
  export const KIRO_STEER_VERIFIED = {
53
58
  "2.27.1": ["tui", "v3"],
54
59
  "2.28.0": ["tui"],
60
+ "2.34.0": ["tui"],
55
61
  };
56
62
  /**
57
63
  * The TUI composer's interrupt mode, read off its own row: the LAST row starting at column 0 with `›` (transcript user
@@ -69,8 +75,10 @@ export const KIRO_STEER_VERIFIED = {
69
75
  * evidence of either an empty or a full box. The toggle key's label is whatever the user bound; it is read past, never
70
76
  * pressed.
71
77
  */
72
- function readKiroComposer(pane) {
73
- const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
78
+ /** #2682: the takeover hold's key (RuntimeDialog.pattern); kiroTakeoverActive decides, this never matches a pane. */
79
+ const KIRO_TAKEOVER_KEY = /(?!)kiro-takeover/;
80
+ /** The composer's `›` row in `rows` and how many typed-text rows continue below it; null when there is none. */
81
+ function findKiroComposerRow(rows) {
74
82
  let i = rows.length - 1;
75
83
  while (i >= 0 && (rows[i] === "" || /^[ \t]{20,}\S/.test(rows[i])))
76
84
  i--;
@@ -82,30 +90,47 @@ function readKiroComposer(pane) {
82
90
  continued++;
83
91
  }
84
92
  const row = rows[i];
85
- if (row === undefined || !/^›[ \t]+\S/.test(row))
93
+ return row !== undefined && /^›[ \t]+\S/.test(row) ? { index: i, continued } : null;
94
+ }
95
+ function readKiroComposer(pane) {
96
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
97
+ const at = findKiroComposerRow(rows);
98
+ if (!at)
86
99
  return null;
100
+ let row = rows[at.index];
87
101
  // A placeholder is one row: with continuation rows below it, this is typed text — unless its first row is another
88
- // mode's placeholder, which is then not ours to read.
89
- if (continued > 0) {
90
- if (/^›[ \t]+(?:Kiro is working\b|ask a question or describe a task\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b)/.test(row))
91
- return null;
92
- return { mode: "text", text: row.replace(/^›[ \t]+/, "") };
102
+ // mode's placeholder. #2682: a narrow pane word-wraps a placeholder onto indented rows; joined, a COMPLETE known form
103
+ // reads as itself (an idle placeholder is then idle, not unreadable); anything else stays unreadable.
104
+ if (at.continued > 0) {
105
+ if (!/^›[ \t]+(?:Kiro is working\b|ask a question or describe a task\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b)/.test(row)) {
106
+ return { mode: "text", text: row.replace(/^›[ \t]+/, "") };
107
+ }
108
+ row = [row, ...rows.slice(at.index + 1, at.index + 1 + at.continued).map(r => r.trim())].join(" ");
109
+ const mode = kiroPlaceholderMode(row);
110
+ return mode ? { mode, text: row.replace(/^›[ \t]+/, "") } : null;
93
111
  }
94
- const working = String.raw `^›[ \t]+Kiro is working(?:[ \t]+·[ \t]+[^·]+?)?[ \t]+·[ \t]+`;
95
112
  const text = row.replace(/^›[ \t]+/, "");
113
+ const mode = kiroPlaceholderMode(row);
114
+ if (mode)
115
+ return { mode, text };
116
+ // Other placeholders (and anything the ASCII glyph set paints) are not typed text.
117
+ if (/^›[ \t]+(?:Kiro is working\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b|ask a question or describe a task (?:\.|enter\b))/.test(row))
118
+ return null;
119
+ return { mode: "text", text };
120
+ }
121
+ /** The empty composer's mode from its whole placeholder (`› …`, one row or wrapped rows joined); null when it is none. */
122
+ function kiroPlaceholderMode(row) {
123
+ const working = String.raw `^›[ \t]+Kiro is working(?:[ \t]+·[ \t]+[^·]+?)?[ \t]+·[ \t]+`;
96
124
  if (new RegExp(`${working}Type to steer[ \\t]+·[ \\t]+[^·]+?[ \\t]+to queue$`).test(row))
97
- return { mode: "steer", text };
125
+ return "steer";
98
126
  if (new RegExp(`${working}Type to queue(?:[ \\t]+·[ \\t]+[^·]+?[ \\t]+to steer)?$`).test(row))
99
- return { mode: "queue", text };
127
+ return "queue";
100
128
  const idle = String.raw `^›[ \t]+ask a question or describe a task`;
101
129
  if (new RegExp(`${idle}[ \\t]+↵$`).test(row)
102
130
  || new RegExp(`${idle}[ \\t]+↵[ \\t]+·[ \\t]+exit plan mode: shift\\+tab$`).test(row)
103
131
  || new RegExp(`${idle}[ \\t]+·[ \\t]+/tangent to go back[ \\t]+·[ \\t]+/tangent ls to view$`).test(row))
104
- return { mode: "idle", text };
105
- // Other placeholders (and anything the ASCII glyph set paints) are not typed text.
106
- if (/^›[ \t]+(?:Kiro is working\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b|ask a question or describe a task (?:\.|enter\b))/.test(row))
107
- return null;
108
- return { mode: "text", text };
132
+ return "idle";
133
+ return null;
109
134
  }
110
135
  export function readKiroSteerComposer(pane) {
111
136
  return readKiroComposer(pane)?.mode ?? null;
@@ -115,6 +140,178 @@ export function readKiroComposerText(pane) {
115
140
  const c = readKiroComposer(pane);
116
141
  return c?.mode === "text" ? c.text : null;
117
142
  }
143
+ /**
144
+ * #2683: the composer as a one-line housekeeping command (`/session-id`, `/title …`) sees it: `""` when it is
145
+ * positively empty (the idle placeholder, nothing else), the typed text of a single-row draft, or null for anything
146
+ * else (busy, another mode, wrapped text, no composer). A typed slash command opens kiro's command menu below the box
147
+ * (live 2.34.0, tui and V3; tests/fixtures/kiro-session-id-2681):
148
+ *
149
+ * `› /session-id`
150
+ * `/session-id Print the current session ID`
151
+ * `──────…`
152
+ * ` esc to cancel · ↵ to select`
153
+ *
154
+ * and its `›` row is read past that menu. Enter there runs the highlighted entry, the command itself.
155
+ */
156
+ export function readKiroCommandComposer(pane) {
157
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
158
+ let i = rows.length - 1;
159
+ while (i >= 0 && (rows[i] === "" || /^[ \t]{20,}\S/.test(rows[i])))
160
+ i--;
161
+ if (i >= 2 && /^ esc to cancel · .*↵ to select$/.test(rows[i]) && /^─{20,}$/.test(rows[i - 1])) {
162
+ let r = i - 2, entries = 0;
163
+ while (r >= 0 && entries < 12 && /^\/[\w-]+ {2,}\S/.test(rows[r])) {
164
+ r--;
165
+ entries++;
166
+ }
167
+ const row = rows[r];
168
+ return entries > 0 && row !== undefined && /^›[ \t]+\/\S/.test(row) ? row.replace(/^›[ \t]+/, "") : null;
169
+ }
170
+ const at = findKiroComposerRow(rows);
171
+ if (!at || at.continued > 0)
172
+ return null;
173
+ const c = readKiroComposer(pane);
174
+ return c?.mode === "idle" ? "" : c?.mode === "text" ? c.text : null;
175
+ }
176
+ /**
177
+ * #2683: kiro's own answer to `/session-id`, where only that answer is painted: the alert at the bottom of the chat,
178
+ * on the rows right above the footer (the divider within 8 rows above the composer, and the trust banner's divider
179
+ * above that one when it is shown). Live 2.34.0, tui and V3 (tests/fixtures/kiro-session-id-2681):
180
+ *
181
+ * `• Session ID: <id>`
182
+ * ` Resume with: kiro-cli --resume-id <id>` (the same id)
183
+ *
184
+ * or `• Session ID: none` alone. It is a transient alert (10 s), not a turn. The same lines anywhere else (a quoted
185
+ * user row is ` › …`, an answer scrolls up) are not read; an agent answer that ends with them would sit in the same
186
+ * place, which is why the daemon also requires that the alert was not there before it typed the command. Null when no
187
+ * such alert is shown.
188
+ */
189
+ export function readKiroSessionIdAlert(pane) {
190
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
191
+ const at = findKiroComposerRow(rows);
192
+ if (!at)
193
+ return null;
194
+ let d = at.index - 1;
195
+ while (d >= 0 && at.index - d <= 8 && !/^─{20,}$/.test(rows[d]))
196
+ d--;
197
+ if (d < 0 || !/^─{20,}$/.test(rows[d]))
198
+ return null;
199
+ if (d >= 2 && /^ \S/.test(rows[d - 1]) && /^─{20,}$/.test(rows[d - 2]))
200
+ d -= 2; // ` Trust All Tools active …`
201
+ if (rows[d - 1] === "• Session ID: none")
202
+ return "none";
203
+ const id = /^ {2}Resume with: kiro-cli --resume-id ([A-Za-z0-9][A-Za-z0-9_.-]{0,199})$/.exec(rows[d - 1] ?? "")?.[1];
204
+ return id !== undefined && id !== "none" && rows[d - 2] === `• Session ID: ${id}` ? id : null;
205
+ }
206
+ /**
207
+ * #2676: a collapsed paste on the composer's row. The TUI shows a paste of more than 10 lines or 500 characters as one
208
+ * chip (2.34.0 tui.js: `n>1 ? "${n} lines" : "${r} chars"`, then `▸`, then the `Press Tab to expand` hint):
209
+ *
210
+ * `› 42 lines ▸ Press Tab to expand` → 42
211
+ * `› 657 chars ▸ Press Tab to expand` → 1 (one line, shown only by its length)
212
+ *
213
+ * `text` is the row's text (readKiroComposerText). Null when it is not such a chip.
214
+ */
215
+ export function readKiroCollapsedPasteLines(text) {
216
+ const m = /^(\d+) (lines|chars) ▸(?:[ \t]+Press Tab to expand)?$/.exec(text.trim());
217
+ if (!m)
218
+ return null;
219
+ return m[2] === "lines" ? Number(m[1]) : 1;
220
+ }
221
+ /**
222
+ * #2682: another program owns kiro's pane, so a paste and an Enter would go into it instead of the chat (captured on
223
+ * 2.34.0, tests/fixtures/kiro-2.34.0-takeover-2682). Read from kiro's own rows first:
224
+ * - a focused `/kvim` panel: `── Neovim · focused · Ctrl+\ to chat · :q to close ──`, or `· the agent is waiting on you:`
225
+ * (unfocused, `Neovim · Ctrl+\ to edit`, the composer has the keys again);
226
+ * - a running `!` command (tui, V3): its PTY takes every keystroke, while the composer still reads busy
227
+ * (`Kiro is working · Type to steer`) — the nearest turn above kiro's footer is ` › !…`, with only its indented
228
+ * output and `• Running...` below (kiroBusyTurn; a turn it cannot see is not called a takeover here, but no busy
229
+ * input is typed into it either: KiroBackend.busyTurnTakesInput);
230
+ * - an editor or pager kiro started (`/editor`, `/transcript`, a full-screen `!vim`/`!less`): no composer row, kiro's
231
+ * temp file `kiro-editor-XXXXXX/prompt.md` or `kiro-raw-XXXXXX/conversation.md` on screen;
232
+ * - legacy: a `!` command running from its prompt row (the bottom row is the prompt with `!…` after the marker: the
233
+ * command took the terminal before kiro painted anything else), and its `/editor` and `/reply` temp file
234
+ * `q_prompt_<uuid>.md` with no prompt row at the bottom;
235
+ * and, only where kiro's input row is not on screen (every engine; legacy's `/editor` and `/reply` run `$EDITOR`), the
236
+ * program's own bottom rows: vi/vim/nvim (`~` rows, its ruler, `-- INSERT --`), less (`:`, `(END)`), more
237
+ * (`--More--`), nano (`GNU nano` and `^X Exit`). `/fullscreen` is not a takeover: the same composer takes input.
238
+ */
239
+ export function kiroTakeoverActive(pane, ui) {
240
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
241
+ if (ui === "legacy") {
242
+ const prompt = kiroLegacyPromptAtBottom(rows);
243
+ if (prompt !== null)
244
+ return prompt.startsWith("!");
245
+ if (rows.some(row => /\/q_prompt_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.md\b/.test(row)))
246
+ return true;
247
+ return fullScreenProgramAtBottom(rows);
248
+ }
249
+ if (rows.some(row => /^─+ Neovim · (?:focused · |the agent is waiting on you: )/.test(row)))
250
+ return true;
251
+ const at = findKiroComposerRow(rows);
252
+ if (at) {
253
+ const mode = readKiroComposer(pane)?.mode;
254
+ return (mode === "steer" || mode === "queue") && kiroBusyTurn(rows, at.index) === "shell";
255
+ }
256
+ if (rows.some(row => /\/kiro-(?:editor|raw)-[A-Za-z0-9]{6}\/(?:prompt|conversation)\.md\b/.test(row)))
257
+ return true;
258
+ return fullScreenProgramAtBottom(rows);
259
+ }
260
+ /**
261
+ * #2682: the legacy UI's prompt row as the last non-blank row — `4% !>` (trust-all) or `4% >`, with an optional
262
+ * `[agent]` prefix and mode glyph, as getBottomReadyPattern — and the text after its marker (a placeholder hint, a
263
+ * draft, or a `!` command it is running); null when the bottom row is not the prompt.
264
+ */
265
+ export function kiroLegacyPromptAtBottom(rows) {
266
+ const last = [...rows].reverse().find(row => row.trim() !== "");
267
+ const m = last === undefined ? null : /^\s*(?:\[[^\]]*\]\s*)?\d+%\s*(?:[^\s\d%!❯>]{1,2}\s+)?(?:!\s?[❯>]|❯|>)(.*)$/.exec(last);
268
+ return m ? m[1].trim() : null;
269
+ }
270
+ /**
271
+ * #2682: what the busy turn nearest above kiro's footer is: "shell" — a `!` command still running (its ` › !` row with
272
+ * only its output and `• Running...` below), "agent" — positive evidence of the agent's turn first (another user row,
273
+ * one of kiro's bullets, its spinner), "unknown" — no footer, or nothing that says either up to the top of the pane.
274
+ * Everything else is passed over, never taken as evidence: the footer and its notes however they wrap, dividers, the
275
+ * tray and tips, indented output.
276
+ */
277
+ function kiroBusyTurn(rows, composer) {
278
+ let d = composer - 1;
279
+ while (d >= 0 && composer - d <= 8 && !/^─{20,}$/.test(rows[d]))
280
+ d--;
281
+ if (d < 0 || !/^─{20,}$/.test(rows[d]))
282
+ return "unknown"; // no footer divider within reach
283
+ for (let r = d - 1; r >= 0; r--) {
284
+ const row = rows[r];
285
+ if (/^ {2}› !/.test(row))
286
+ return "shell";
287
+ if (/^ {2}› /.test(row) || /^• (?!Running\.\.\.$)\S/.test(row) || /^\S[ \t]+(?:Thinking|Working)(?:\.{3}|…)/.test(row))
288
+ return "agent";
289
+ }
290
+ return "unknown";
291
+ }
292
+ /**
293
+ * #2682: whether a busy composer's typed input goes to kiro's own turn: only when that turn is seen to be the agent's.
294
+ * A `!` command's PTY would take it, and a turn it cannot see might be one.
295
+ */
296
+ export function kiroBusyTurnTakesInput(pane) {
297
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
298
+ const at = findKiroComposerRow(rows);
299
+ return at !== null && kiroBusyTurn(rows, at.index) === "agent";
300
+ }
301
+ /** #2682: a full-screen editor or pager's own bottom rows (vi/vim/nvim, less, more, nano), whoever started it. */
302
+ function fullScreenProgramAtBottom(rows) {
303
+ const shown = rows.filter(row => row.trim() !== "");
304
+ const last = shown[shown.length - 1]?.trim() ?? "";
305
+ if (last === ":" || /\(END\)$/.test(last) || /^--More--/.test(last))
306
+ return true; // less, more
307
+ if (/^-- (?:INSERT|REPLACE|VISUAL(?: LINE| BLOCK)?) --/.test(last))
308
+ return true; // vi modes
309
+ if (/\s\d+,\d+(?:-\d+)?\s+(?:All|Top|Bot|\d+%)$/.test(last))
310
+ return true; // vi ruler
311
+ if (shown.slice(-7, -1).filter(row => row === "~").length >= 3)
312
+ return true; // vi's empty lines
313
+ return shown.slice(-2).some(row => /\^X Exit/.test(row)) && rows.some(row => /^\s*GNU nano\b/.test(row)); // nano
314
+ }
118
315
  const UNKNOWN_KIRO_COMPATIBILITY = {
119
316
  supportsLegacyUi: false,
120
317
  supportsTui: false,
@@ -133,11 +330,12 @@ const warnedVersionGateCacheKeys = new Set();
133
330
  * This broad test is only a safety hold: it must never select an option or
134
331
  * claim a service outage. A ready prompt below the picker makes it history.
135
332
  */
333
+ const KIRO_MODEL_PICKER_HEADER = /^\s*Select model(?: \(type to search\):|:\s+type to search)\s*$/m;
136
334
  function kiroModelPickerAtTail(pane) {
137
335
  const lines = pane.replace(/\r/g, "").split("\n");
138
336
  let pickerIndex = -1;
139
337
  for (let index = lines.length - 1; index >= 0; index--) {
140
- if (/^\s*Select model \(type to search\):\s*$/.test(lines[index])) {
338
+ if (KIRO_MODEL_PICKER_HEADER.test(lines[index])) {
141
339
  pickerIndex = index;
142
340
  break;
143
341
  }
@@ -145,7 +343,8 @@ function kiroModelPickerAtTail(pane) {
145
343
  if (pickerIndex < 0)
146
344
  return false;
147
345
  const tail = lines.slice(pickerIndex + 1);
148
- if (tail.some(line => /^\s*\d+%[^\n]*[!❯>]\s+\S/.test(line)))
346
+ const composer = readKiroSteerComposer(tail.join("\n"));
347
+ if (tail.some(line => /^\s*\d+%[^\n]*[!❯>]\s+\S/.test(line)) || (composer !== null && composer !== "text"))
149
348
  return false;
150
349
  return tail.some(line => /^\s*[>❯›]\s*\*?\s*\S/.test(line));
151
350
  }
@@ -330,6 +529,205 @@ function kiroWorkspaceTrustDialogs(trustAll) {
330
529
  },
331
530
  ];
332
531
  }
532
+ const KIRO_MCP_INPUT_HEADER = /^\s*Input requested · \S.*$/;
533
+ const KIRO_MCP_INPUT_FOOTER = /^\s*esc to cancel · ↑↓ to navigate · enter to select(?: · ctrl\+s to submit)?$/;
534
+ /**
535
+ * #2587: kiro-cli 2.34.0's V3 engine answers an MCP server's `elicitation/create` (a tool asking the user for input)
536
+ * with a card in place of the composer: a divider, `Input requested · <server>`, a divider, the server's message and
537
+ * one row per form field then Submit / Decline (form mode) — or Host, URL and Open in browser / Copy URL / I'll open it
538
+ * myself / Decline / Cancel (url mode) — with exactly one `❯` cursor row, a divider, and the key hints as the last row
539
+ * (tests/fixtures/kiro-2.34.0-audit). Bottom-anchored: the card is live only while it is what the pane ends with. The
540
+ * answer can carry credentials or a sign-in, so it is the person's: never a key, and no delivery's Enter may land in it.
541
+ */
542
+ export function kiroMcpInputRequestActive(pane) {
543
+ const rows = pane.replace(/\r/g, "").split("\n").map(row => row.trimEnd());
544
+ let last = rows.length - 1;
545
+ while (last >= 0 && !rows[last].trim())
546
+ last--;
547
+ if (last < 0 || !KIRO_MCP_INPUT_FOOTER.test(rows[last]))
548
+ return false;
549
+ const divider = (row) => row !== undefined && /^─{3,}$/.test(row.trim());
550
+ if (!divider(rows[last - 1]))
551
+ return false;
552
+ let header = -1;
553
+ for (let index = last - 2; index >= 1; index--) {
554
+ if (KIRO_MCP_INPUT_HEADER.test(rows[index])) {
555
+ header = index;
556
+ break;
557
+ }
558
+ }
559
+ if (header < 0 || !divider(rows[header - 1]) || !divider(rows[header + 1]))
560
+ return false;
561
+ const body = rows.slice(header + 2, last - 1);
562
+ // A V3 composer (`›` at column 0) inside means the card is history. A lone `>` is a form field's own input caret.
563
+ if (body.some(row => divider(row) || /^›(?:[ \t]|$)/.test(row)))
564
+ return false;
565
+ return body.filter(row => /^\s*❯\s+\S/.test(row)).length === 1;
566
+ }
567
+ const KIRO_QUESTION_HEADER = /^\s*(?:\S.* > )?Question$/;
568
+ const KIRO_QUESTION_FOOTER = /^\s*esc to (?:cancel|go back) · ↑↓ to navigate(?: · space to toggle)? · ↵ to submit$/;
569
+ /**
570
+ * #2592: kiro's V3 engine shows the agent's question to the user (`question_request`) as a card in place of the
571
+ * composer: a divider, `Question` (or `<name> > Question`), the question, numbered options (a description row under
572
+ * one, `(recommended)` after one) and `Type a different answer…`, a divider, and the key hints as the last row — or, in
573
+ * an option's sub-options, `[✓] <title>` rows and `esc to go back · … · space to toggle · ↵ to submit`
574
+ * (tests/fixtures/kiro-question-2592; unchanged from 2.33.1 to 2.34.0). Any typed character turns the card into a free
575
+ * answer and Enter submits it, so a delivered message would become the answer to a question nobody read. The person
576
+ * answers it in the pane (the user's decision on #2592); it is never keyed. Bottom-anchored like
577
+ * kiroMcpInputRequestActive: the header is the row right under the nearest divider above the key hints' divider.
578
+ */
579
+ export function kiroQuestionCardActive(pane) {
580
+ const rows = pane.replace(/\r/g, "").split("\n").map(row => row.trimEnd());
581
+ let last = rows.length - 1;
582
+ while (last >= 0 && !rows[last].trim())
583
+ last--;
584
+ if (last < 0 || !KIRO_QUESTION_FOOTER.test(rows[last]))
585
+ return false;
586
+ const divider = (row) => row !== undefined && /^─{3,}$/.test(row.trim());
587
+ if (!divider(rows[last - 1]))
588
+ return false;
589
+ let top = last - 2;
590
+ while (top >= 0 && !divider(rows[top]))
591
+ top--;
592
+ if (top < 0 || !KIRO_QUESTION_HEADER.test(rows[top + 1] ?? ""))
593
+ return false;
594
+ const body = rows.slice(top + 2, last - 1);
595
+ // The question, then the options: a V3 composer (`›` at column 0) among them means the card is history.
596
+ if (body.length < 2 || body.some(row => /^›(?:[ \t]|$)/.test(row)))
597
+ return false;
598
+ return body.filter(row => /^\s*❯\s+\S/.test(row)).length === 1;
599
+ }
600
+ /** #2675: the live card's rows — the header row, the body between it and the key hints' divider, and the key hints. */
601
+ function kiroCardRows(pane, headerAt) {
602
+ const rows = pane.replace(/\r/g, "").split("\n").map(row => row.trimEnd());
603
+ let last = rows.length - 1;
604
+ while (last >= 0 && !rows[last].trim())
605
+ last--;
606
+ const header = headerAt(rows, last);
607
+ return header < 0 ? null : { header: rows[header].trim(), body: rows.slice(header + 1, last - 1), footer: rows[last].trim(), width: rows[last - 1].trim().length };
608
+ }
609
+ const KIRO_QUESTION_OPTION_ROW = /^ (❯| ) (\d+)\. (.*?)( \(recommended\))?$/;
610
+ const KIRO_QUESTION_SUBOPTION_ROW = /^ (❯| ) \[(✓| )\] (.*)$/;
611
+ const KIRO_QUESTION_FREE_ROW = /^ (❯| ) (Type a different answer…|or type feedback to request changes)$/;
612
+ /**
613
+ * #2675: the live Question card (see kiroQuestionCardActive), read for a person to answer from the chat: the question,
614
+ * the numbered options (`(recommended)` and the description row under one), the typed-answer row, and where the `❯`
615
+ * is (tui.js `A0`; tests/fixtures/kiro-question-2592). On a sub-options page the rows are the `[✓]` boxes; while an
616
+ * answer is being typed the last row is `❯ <text>`. A row the shapes above do not account for is a wrapped title (the
617
+ * card's own indent) or description (deeper), joined to the row it continues; one before any option belongs to the
618
+ * question. Null when the card is not live, or when it is in a shape this reader does not know.
619
+ */
620
+ export function readKiroQuestionCard(pane) {
621
+ if (!kiroQuestionCardActive(pane))
622
+ return null;
623
+ const card = kiroCardRows(pane, (rows, last) => {
624
+ let top = last - 2;
625
+ while (top >= 0 && !/^─{3,}$/.test(rows[top].trim()))
626
+ top--;
627
+ return top + 1;
628
+ });
629
+ if (!card)
630
+ return null;
631
+ const page = /space to toggle/.test(card.footer) ? "suboptions" : "options";
632
+ const question = [];
633
+ const options = [];
634
+ let cursor = null, freeText = false, typed = null, freeRow = false;
635
+ for (const row of card.body) {
636
+ const option = page === "options" ? KIRO_QUESTION_OPTION_ROW.exec(row) : null;
637
+ const sub = page === "suboptions" ? KIRO_QUESTION_SUBOPTION_ROW.exec(row) : null;
638
+ const free = page === "options" ? KIRO_QUESTION_FREE_ROW.exec(row) : null;
639
+ if (freeRow)
640
+ return null; // nothing follows the typed-answer row
641
+ if (option && Number(option[2]) === options.length + 1) {
642
+ if (option[1] === "❯")
643
+ cursor = options.length;
644
+ options.push({ n: options.length + 1, title: option[3], recommended: option[4] !== undefined, description: null });
645
+ }
646
+ else if (sub) {
647
+ if (sub[1] === "❯")
648
+ cursor = options.length;
649
+ options.push({ n: options.length + 1, title: sub[3], recommended: false, description: null, checked: sub[2] === "✓" });
650
+ }
651
+ else if (free) {
652
+ freeText = true;
653
+ freeRow = true;
654
+ if (free[1] === "❯")
655
+ cursor = options.length;
656
+ }
657
+ else if (page === "options" && options.length > 0 && /^ ❯ /.test(row)) {
658
+ freeText = true;
659
+ freeRow = true;
660
+ typed = row.slice(3);
661
+ }
662
+ else if (!row.trim()) {
663
+ continue;
664
+ }
665
+ else if (options.length === 0) {
666
+ question.push(row.replace(/^ /, ""));
667
+ }
668
+ else {
669
+ const lastOption = options[options.length - 1];
670
+ if (/^ {5,}\S/.test(row))
671
+ lastOption.description = lastOption.description === null ? row.trim() : `${lastOption.description} ${row.trim()}`;
672
+ else if (lastOption.description === null)
673
+ lastOption.title = `${lastOption.title} ${row.trim()}`;
674
+ else
675
+ return null;
676
+ }
677
+ }
678
+ if (options.length === 0 || question.length === 0)
679
+ return null;
680
+ const typing = typed !== null;
681
+ if (typing ? cursor !== null : cursor === null)
682
+ return null;
683
+ const titles = options.map(option => `${option.n}. ${option.title}`).join("\n");
684
+ return {
685
+ kind: "question", identity: `${card.header}\n${question.join("\n")}\n${page === "suboptions" ? "sub" : "options"}\n${titles}`,
686
+ title: card.header, question: question.join("\n").trim(), page: typing ? "freetext" : page, options, freeText, cursor, typed, rowWidth: card.width,
687
+ };
688
+ }
689
+ const KIRO_MCP_INPUT_ACTIONS = new Set(["Submit", "Decline", "Open in browser", "Copy URL", "I'll open it myself", "Cancel"]);
690
+ /**
691
+ * #2675: the live MCP `Input requested` card (see kiroMcpInputRequestActive), read for the chat: the server, the body
692
+ * as painted, with the `URL:` row masked (a sign-in link stays in the pane), its action rows, and the `❯` row. Its
693
+ * identity is the server, the mode, the message's first row and the actions: moving or filling in a field keeps it.
694
+ */
695
+ export function readKiroMcpInputCard(pane) {
696
+ if (!kiroMcpInputRequestActive(pane))
697
+ return null;
698
+ const card = kiroCardRows(pane, (rows, last) => {
699
+ for (let index = last - 2; index >= 1; index--)
700
+ if (KIRO_MCP_INPUT_HEADER.test(rows[index]))
701
+ return index;
702
+ return -1;
703
+ });
704
+ if (!card)
705
+ return null;
706
+ // The URL row is masked, and so is every row after it up to the next action row: a long URL wraps (#2677 review).
707
+ let inUrl = false;
708
+ const body = card.body.slice(1).flatMap(row => {
709
+ if (/^\s*URL: /.test(row)) {
710
+ inUrl = true;
711
+ return [row.replace(/URL: .*$/, "URL: (shown in the instance pane)")];
712
+ }
713
+ if (inUrl && KIRO_MCP_INPUT_ACTIONS.has(row.replace(/^ ❯ /, " ").trim()))
714
+ inUrl = false;
715
+ return inUrl ? [] : [row];
716
+ });
717
+ const strip = (row) => row.replace(/^ ❯ /, " ").trim();
718
+ const cursorRow = body.find(row => /^\s*❯\s+\S/.test(row));
719
+ if (!cursorRow)
720
+ return null;
721
+ return {
722
+ kind: "input", server: card.header.replace(/^Input requested · /, ""),
723
+ mode: /ctrl\+s to submit/.test(card.footer) ? "form" : "url",
724
+ rows: body.map(row => row.replace(/^ ❯ /, " ").replace(/^ {3}/, "")).filter(row => row.trim()),
725
+ actions: body.map(strip).filter(row => KIRO_MCP_INPUT_ACTIONS.has(row)),
726
+ cursorRow: strip(cursorRow),
727
+ // The focused field paints extra rows (its value, its caret) and a typed value changes its row: neither is another request.
728
+ identity: `${card.header}\n${/ctrl\+s to submit/.test(card.footer) ? "form" : "url"}\n${strip(body[0] ?? "")}\n${body.map(strip).filter(row => KIRO_MCP_INPUT_ACTIONS.has(row)).join("|")}`,
729
+ };
730
+ }
333
731
  /**
334
732
  * Kiro's service-unavailable path opens the same picker as `/model`, but only
335
733
  * the former needs incident escalation. The runtime scanner and delivery gate
@@ -342,7 +740,7 @@ export function kiroUnavailableModelPickerActive(pane) {
342
740
  const lines = pane.replace(/\r/g, "").split("\n");
343
741
  let pickerIndex = -1;
344
742
  for (let i = lines.length - 1; i >= 0; i--) {
345
- if (/^\s*Select model \(type to search\):\s*$/.test(lines[i])) {
743
+ if (KIRO_MODEL_PICKER_HEADER.test(lines[i])) {
346
744
  pickerIndex = i;
347
745
  break;
348
746
  }
@@ -634,6 +1032,17 @@ function compatibilityFromHelp(version, help) {
634
1032
  source: "help",
635
1033
  };
636
1034
  }
1035
+ /**
1036
+ * #2598 (D1): kiro-cli-chat skips its self-update when this is set (2.30.0 and 2.34.0 both read it). AgEnD pins the
1037
+ * kiro version it has audited; an in-place update can swap the engine a running fleet depends on (3.0 drops legacy).
1038
+ * Every chat AgEnD starts carries it: the instance launch (a shell prefix) and the probes (their child env). The
1039
+ * host-wide `app.disableAutoupdates` setting is the operator's half; this holds even where that was never set.
1040
+ */
1041
+ export const KIRO_NO_AUTO_UPDATE = "KIRO_NO_AUTO_UPDATE";
1042
+ /** A kiro child's environment: the given one (default: ours) with self-update off. */
1043
+ export function kiroChildEnv(base = process.env) {
1044
+ return { ...base, [KIRO_NO_AUTO_UPDATE]: "1" };
1045
+ }
637
1046
  /**
638
1047
  * Probe once per binary generation. A version AgEnD has run (<= TESTED_MAX)
639
1048
  * is answered from the table without a second CLI call; anything newer, or a
@@ -645,6 +1054,7 @@ export function probeKiroCliCompatibility(binaryPath, run = (binary, args) => me
645
1054
  encoding: "utf-8",
646
1055
  timeout: 5000,
647
1056
  stdio: ["ignore", "pipe", "ignore"],
1057
+ env: kiroChildEnv(),
648
1058
  }))) {
649
1059
  return measureSyncWork("kiro.compatibilitySync", () => probeKiroCliCompatibilitySync(binaryPath, run));
650
1060
  }
@@ -673,6 +1083,8 @@ function nextPatch(version) {
673
1083
  const [major, minor, patch] = parseSemver(version);
674
1084
  return `${major}.${minor}.${patch + 1}`;
675
1085
  }
1086
+ /** #2598 B3: engines in the order a conversation can move between them (never back). */
1087
+ const KIRO_ENGINE_RANK = { legacy: 1, tui: 2, v3: 3 };
676
1088
  /** Binary generations already told they cannot isolate (once each). */
677
1089
  const warnedNoInstanceAgentKeys = new Set();
678
1090
  /** Instances already told about an untagged steering file (once each). */
@@ -755,6 +1167,34 @@ export function readActiveKiroAgent(pane, ui) {
755
1167
  }
756
1168
  return null;
757
1169
  }
1170
+ /** Model on Kiro 2.34's live footer (status, cwd, optional branch, blank, idle composer).
1171
+ * Separate from readActiveKiroAgent: do not widen the disabled agent-switch action's reader.
1172
+ */
1173
+ export function readActiveKiroModel(pane, expectedAgent) {
1174
+ if (readKiroSteerComposer(pane) !== "idle")
1175
+ return null;
1176
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, "")), at = findKiroComposerRow(rows);
1177
+ if (!at)
1178
+ return null;
1179
+ let cwd = false, branch = false;
1180
+ // Footer only. Never search backwards through arbitrary output or a model picker for a matching model.
1181
+ for (let i = at.index - 1; i >= Math.max(0, at.index - 8); i--) {
1182
+ const row = rows[i];
1183
+ if (!row.trim() || RULE_ROW.test(row))
1184
+ continue;
1185
+ if (!branch && /^\s*\([^()\r\n]{1,128}\)\s*$/.test(row)) {
1186
+ branch = true;
1187
+ continue;
1188
+ }
1189
+ if (!cwd && /^\s*(?:\/[^\x00-\x1f]*|~(?:\/[^\x00-\x1f]*)?)\s*$/.test(row)) {
1190
+ cwd = true;
1191
+ continue;
1192
+ }
1193
+ const status = /^\s*(\S+) · ([A-Za-z0-9_.:/-]{1,128}) · .+$/.exec(row);
1194
+ return cwd && status && (expectedAgent === undefined || status[1] === expectedAgent) ? status[2] : null;
1195
+ }
1196
+ return null;
1197
+ }
758
1198
  export function planKiroLaunch(ui, compat) {
759
1199
  const version = compat.version ?? "of unknown version";
760
1200
  if (ui === "v3")
@@ -885,6 +1325,9 @@ export const KIRO_RATE_LIMIT_SCREEN = new RegExp(KIRO_MESSAGE_LEAD + wrapToleran
885
1325
  /** #1636: kiro's monthly cap (MonthlyLimitReached branch); see getErrorPatterns. */
886
1326
  export const KIRO_REQUEST_LIMIT_SCREEN = new RegExp(KIRO_MESSAGE_LEAD + "(?:" + ["Hourly", "Daily", "Weekly", "Monthly"].map(wrapTolerant).join("|") + ")"
887
1327
  + wrapTolerant(" request limit reached") + "(?:[ \\t]*$|" + wrapTolerant(" - The limits reset on") + "\\b)", "m");
1328
+ /** #2598: Kiro 2.34 lists a carried UUID in both V2 and classic; this proves presence, not a missing session. */
1329
+ export const KIRO_AMBIGUOUS_RESUME_SCREEN = new RegExp(wrapTolerant('Error: Session id "') + '[A-Za-z0-9_.\\n-]{1,256}' + wrapTolerant('" is ambiguous (')
1330
+ + "\\d+" + wrapTolerant(" matches)."), "m");
888
1331
  /** Startup budget for a `--resume` launch (60% first output, 40% ready). */
889
1332
  export const KIRO_RESUME_STARTUP_BUDGET_MS = 60_000;
890
1333
  export class KiroBackend {
@@ -913,6 +1356,8 @@ export class KiroBackend {
913
1356
  activeTrustAll = true;
914
1357
  /** Version-gate notice for the launch just built (consumeLaunchWarning). */
915
1358
  launchWarning = null;
1359
+ /** #2648: what this launch's model came from, for a model error to be judged by (decideKiroModelError). */
1360
+ launchModels = null;
916
1361
  /**
917
1362
  * What this launch attempt resumes and as which agent (#906), resolved once per attempt in writeConfig — it claims
918
1363
  * conversations and records fresh starts — and read by the buildCommand that follows it.
@@ -923,7 +1368,10 @@ export class KiroBackend {
923
1368
  /** #1218: the last launch built in full (cleared when a build starts, set only when it returns). */
924
1369
  builtLaunch = null;
925
1370
  preparedV2Store = null;
1371
+ /** #2648: the saved model of the v2 conversation this instance has recorded, read in prepareLaunch (off the loop). */
1372
+ preparedV2Model = null;
926
1373
  storePreparation = 0;
1374
+ preparedCarry = null;
927
1375
  storeKey(config) {
928
1376
  return JSON.stringify([config.instanceName, config.workingDirectory, kiroCanonicalDirectory(config.workingDirectory), config.kiroUi,
929
1377
  resolveCredentialProfile(config.backendOptions), !!config.skipResume, getAgendHome(), kiroV2SessionsDir(), kiroCanonicalDirectory(kiroV2SessionsDir())]);
@@ -939,6 +1387,8 @@ export class KiroBackend {
939
1387
  async prepareLaunch(config) {
940
1388
  const preparation = ++this.storePreparation;
941
1389
  this.preparedV2Store = null;
1390
+ this.preparedV2Model = null;
1391
+ this.preparedCarry = null;
942
1392
  if (!config || config.kiroUi !== "tui")
943
1393
  return;
944
1394
  if (!this.fixedCompatibility)
@@ -949,11 +1399,63 @@ export class KiroBackend {
949
1399
  return;
950
1400
  const opts = { instance: config.instanceName, engine: "v2", workingDirectory: config.workingDirectory,
951
1401
  credentialProfile: resolveCredentialProfile(config.backendOptions), skipResume: config.skipResume, agendHome: getAgendHome() };
1402
+ if (!config.skipResume && this.needsCarryPreparation()) {
1403
+ try {
1404
+ this.preparedCarry = { key, error: null };
1405
+ if (kiroCarryCandidate(opts)) {
1406
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), carry: {
1407
+ ...opts, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(opts.credentialProfile),
1408
+ } });
1409
+ if (preparation !== this.storePreparation || key !== this.storeKey(config))
1410
+ return;
1411
+ this.preparedCarry = { key, error: reply.kind === "unreadable" ? reply.detail : null };
1412
+ if (reply.kind === "unreadable")
1413
+ return;
1414
+ if (reply.carry?.archivePath)
1415
+ this.addLaunchWarning(t("kiro.archive_done", reply.carry.archivePath));
1416
+ if (reply.carry?.repaired)
1417
+ this.addLaunchWarning(t("kiro.carry_pin_repaired"));
1418
+ }
1419
+ }
1420
+ catch (err) {
1421
+ this.preparedCarry = { key, error: err.message };
1422
+ return;
1423
+ }
1424
+ }
952
1425
  if (kiroIdentityNeedsStore(opts)) {
953
1426
  const store = await listKiroV2Sessions(config.workingDirectory, kiroV2SessionsDir());
954
1427
  if (preparation === this.storePreparation && key === this.storeKey(config))
955
1428
  this.preparedV2Store = { key, store };
956
1429
  }
1430
+ // #2648: only for an instance with no model of its own, and only the conversation it has recorded (the resolver
1431
+ // in writeConfig may pick another — then nothing is foreseen, and a model error is handled after the launch).
1432
+ const recorded = !config.model && !config.skipResume ? kiroRecordedConversation(opts) : null;
1433
+ if (recorded) {
1434
+ const model = await readKiroV2SavedModel(kiroV2SessionsDir(), recorded);
1435
+ if (preparation === this.storePreparation && key === this.storeKey(config))
1436
+ this.preparedV2Model = { key, id: recorded, model };
1437
+ }
1438
+ }
1439
+ needsCarryPreparation() {
1440
+ const v = /(?:^|\s)(\d+)\.(\d+)\.(\d+)/.exec(this.compatibility.version ?? "");
1441
+ return !!v && (Number(v[1]) > 2 || (Number(v[1]) === 2 && Number(v[2]) >= 34));
1442
+ }
1443
+ assertCarryPrepared(config) {
1444
+ if (config.kiroUi !== "tui" || config.skipResume || !this.needsCarryPreparation())
1445
+ return;
1446
+ const opts = { agendHome: getAgendHome(), instance: config.instanceName, workingDirectory: config.workingDirectory,
1447
+ credentialProfile: resolveCredentialProfile(config.backendOptions) };
1448
+ let required;
1449
+ try {
1450
+ required = !!kiroCarryCandidate(opts);
1451
+ }
1452
+ catch (err) {
1453
+ throw new ResumeBlockedError(t("kiro.archive_failed", err.message), t("kiro.archive_remedy"));
1454
+ }
1455
+ if (this.preparedCarry?.key === this.storeKey(config) && this.preparedCarry.error)
1456
+ throw new ResumeBlockedError(t("kiro.archive_failed", this.preparedCarry.error), t("kiro.archive_remedy"));
1457
+ if (required && this.preparedCarry?.key !== this.storeKey(config))
1458
+ throw new ResumeBlockedError(t("kiro.archive_failed", "carry preparation is missing"), t("kiro.archive_remedy"));
957
1459
  }
958
1460
  constructor(instanceDir, compatibility) {
959
1461
  this.instanceDir = instanceDir;
@@ -1009,6 +1511,10 @@ export class KiroBackend {
1009
1511
  readSteerComposerText(pane) {
1010
1512
  return this.activeUi === "legacy" ? null : readKiroComposerText(pane);
1011
1513
  }
1514
+ /** #2676: the line count a collapsed paste chip on the composer's row names (readKiroCollapsedPasteLines). */
1515
+ readSteerComposerPasteLines(text) {
1516
+ return this.activeUi === "legacy" ? null : readKiroCollapsedPasteLines(text);
1517
+ }
1012
1518
  /**
1013
1519
  * Live evidence (kiro-cli 2.21.0, 2026-09-03): `--resume` prints nothing
1014
1520
  * until the conversation comes back from the backend ("Picking up where we
@@ -1020,6 +1526,18 @@ export class KiroBackend {
1020
1526
  getStartupBudgetMs(ctx) {
1021
1527
  return ctx.resume ? KIRO_RESUME_STARTUP_BUDGET_MS : undefined;
1022
1528
  }
1529
+ resumeBlockedPattern() { return KIRO_AMBIGUOUS_RESUME_SCREEN; }
1530
+ retainedResumeBlock() {
1531
+ const plan = this.activePlan;
1532
+ if (!plan || plan.engine !== "v2" || plan.mode !== "resume" || !plan.id || !this.activeInstance)
1533
+ return null;
1534
+ let carried = true;
1535
+ try {
1536
+ carried = kiroCarryCandidate({ agendHome: getAgendHome(), instance: this.activeInstance, workingDirectory: plan.cwd, credentialProfile: plan.credentialProfile })?.id === plan.id;
1537
+ }
1538
+ catch { /* lost ownership proof is also a hold, never authority to discard history */ }
1539
+ return carried ? { reason: t("kiro.carry_resume_failed"), remedy: t("kiro.archive_remedy") } : null;
1540
+ }
1023
1541
  retriesResumeOnStartupFailure() {
1024
1542
  // A resume miss is far more often "the backend was slow" than "the session
1025
1543
  // is broken"; one more attempt before abandoning the conversation is cheap.
@@ -1054,7 +1572,14 @@ export class KiroBackend {
1054
1572
  // V3 has a different engine and no verified completion signal yet (#1144).
1055
1573
  this.replyGuardOptedIn = false;
1056
1574
  this.builtLaunch = null;
1575
+ this.assertCarryPrepared(config);
1057
1576
  const ui = config.kiroUi ?? "legacy";
1577
+ // #2598 B3: never back onto an older engine than this instance has run on — that resumes the frozen copy and loses
1578
+ // everything done since. Before any probe: no kiro-cli flag or version changes the answer.
1579
+ const floor = kiroEngineFloor(getAgendHome(), config.instanceName);
1580
+ if (floor && KIRO_ENGINE_RANK[ui] < KIRO_ENGINE_RANK[floor === "v2" ? "tui" : "v3"]) {
1581
+ throw new UnsupportedCliError(t("kiro.engine_floor", config.instanceName, ui, floor === "v2" ? "tui" : "v3"));
1582
+ }
1058
1583
  // Every launch re-reads the binary generation: a crash-respawn reuses this
1059
1584
  // backend, and kiro-cli may have replaced itself in place since the last
1060
1585
  // launch (an auto-update to a release without this instance's engine).
@@ -1075,7 +1600,8 @@ export class KiroBackend {
1075
1600
  const plan = planKiroLaunch(ui, this.compatibility);
1076
1601
  if (plan.kind === "refuse")
1077
1602
  throw new UnsupportedCliError(plan.reason);
1078
- let cmd = `${this.binaryPath} chat`;
1603
+ // #2598: a shell assignment, so it reaches kiro-cli-chat through `kiro-cli chat` and any credential-home prefix.
1604
+ let cmd = `${KIRO_NO_AUTO_UPDATE}=1 ${this.binaryPath} chat`;
1079
1605
  for (const flag of plan.flags)
1080
1606
  cmd += ` ${flag}`;
1081
1607
  this.noteVersionGate();
@@ -1119,17 +1645,32 @@ export class KiroBackend {
1119
1645
  if (id)
1120
1646
  cmd += ` --resume-id ${shellQuote(id)}`;
1121
1647
  }
1122
- else if (!config.skipResume) {
1123
- // Legacy mode, classic/TUI: the boolean form — kiro resumes the directory's newest conversation. Not isolated
1648
+ else if (!config.skipResume && ui === "legacy") {
1649
+ // Legacy mode, classic: the boolean form — kiro resumes the directory's newest conversation. Not isolated
1124
1650
  // between instances sharing the directory (#1410); an instance launches like this only without the
1125
1651
  // per-instance plan (an old kiro-cli, or a conversation store AgEnD could not read on adoption).
1126
1652
  cmd += " --resume";
1127
1653
  }
1654
+ else if (!config.skipResume && ui === "tui") {
1655
+ // #2598 B3: never the boolean form under the TUI — it takes the directory's newest conversation from EITHER
1656
+ // engine, and on a classic one makes a TUI fork of it (the wrong conversation, possibly another instance's). A
1657
+ // TUI launch without its per-instance plan starts fresh instead, and says so.
1658
+ this.addLaunchWarning(t("kiro.tui_no_plan_fresh"));
1659
+ }
1660
+ const resumed = lp.mode === "resume" && lp.id && (lp.engine === "v1" || lp.engine === "v2") ? { engine: lp.engine, id: lp.id } : null;
1661
+ const fallbackModel = !config.model && resumed && config.instanceDir
1662
+ ? this.retiredModelFallback(config, resumed, credentialProfile) : null;
1663
+ this.launchModels = config.instanceDir
1664
+ ? { instanceDir: config.instanceDir, ownModel: config.model ?? null, launchModel: config.model ?? fallbackModel, conversation: resumed?.id ?? null }
1665
+ : null;
1128
1666
  if (config.model) {
1129
1667
  const model = validateModel(config.model);
1130
1668
  warnIfModelMismatch("kiro-cli", model);
1131
1669
  cmd += ` --model ${shellQuote(model)}`;
1132
1670
  }
1671
+ else if (fallbackModel) {
1672
+ cmd += ` --model ${shellQuote(validateModel(fallbackModel))}`;
1673
+ }
1133
1674
  if (config.effort) {
1134
1675
  const effort = validateEffort(config.effort);
1135
1676
  if (this.compatibility.supportsEffortFlag) {
@@ -1219,6 +1760,7 @@ export class KiroBackend {
1219
1760
  return true;
1220
1761
  }
1221
1762
  writeConfig(config) {
1763
+ this.assertCarryPrepared(config);
1222
1764
  // WORKAROUND: kiro-cli ignores the "env" block of an MCP server entry — the server subprocess inherits the fleet
1223
1765
  // manager's env, with a stale AGEND_SOCKET_PATH from whichever daemon wrote last. Each server therefore runs
1224
1766
  // through a wrapper script that exports this instance's env and execs the real server.
@@ -1290,6 +1832,59 @@ export class KiroBackend {
1290
1832
  this.addLaunchWarning(t("kiro.untagged_steering"));
1291
1833
  }
1292
1834
  }
1835
+ /**
1836
+ * #2648 steps 1 and 3: the model a resumed conversation is launched on instead of its saved one, or null to launch as
1837
+ * before. Only for an instance with no model of its own. The conversation's current saved model decides, every
1838
+ * launch: one kiro's cached list no longer has is switched to kiro's default; one it has — or, without a list, one
1839
+ * other than the model that failed (a later /model choice, or the default kiro saved after a switch) — is launched
1840
+ * as saved, even after an earlier switch (#2649 review). Only when the saved model cannot be read does a recorded
1841
+ * switch (step 2's restart) still apply. Announced once per conversation; the record also keeps that dedupe.
1842
+ */
1843
+ retiredModelFallback(config, resumed, credentialProfile) {
1844
+ const records = readKiroModelRecords(config.instanceDir);
1845
+ let entry = records.switched[resumed.id];
1846
+ const known = readKiroKnownModels(getAgendHome());
1847
+ const prepared = this.preparedV2Model;
1848
+ const saved = resumed.engine === "v1"
1849
+ ? readKiroV1SavedModel({ id: resumed.id, workingDirectory: config.workingDirectory, dbPath: kiroV1DbPath(credentialProfile) })
1850
+ : prepared && prepared.key === this.storeKey(config) && prepared.id === resumed.id ? prepared.model : null;
1851
+ if (saved) {
1852
+ // Step 2's restart recorded no model: the one saved now is the one that failed — unless it is the default the
1853
+ // switch put there (kiro saves the model a launch ran on).
1854
+ if (entry && entry.from === null && saved !== entry.to)
1855
+ entry.from = saved;
1856
+ const supported = known ? known.models.has(saved) : !!entry && (saved === entry.to || (entry.from !== null && saved !== entry.from));
1857
+ if (supported || (!known && !entry)) {
1858
+ if (entry && entry.from !== saved)
1859
+ this.saveModelRecords(config.instanceDir, records);
1860
+ return null;
1861
+ }
1862
+ if (!entry || entry.from !== saved)
1863
+ entry = records.switched[resumed.id] = { from: saved, to: known?.defaultModel ?? entry?.to ?? KIRO_FALLBACK_MODEL, announced: entry?.announced ?? false };
1864
+ }
1865
+ else if (!entry) {
1866
+ return null;
1867
+ }
1868
+ if (!entry.announced) {
1869
+ this.addLaunchWarning(t("kiro.retired_model_switched", config.instanceName, entry.from ?? t("kiro.retired_model_unknown"), entry.to));
1870
+ entry.announced = true;
1871
+ }
1872
+ this.saveModelRecords(config.instanceDir, records);
1873
+ return entry.to;
1874
+ }
1875
+ saveModelRecords(instanceDir, records) {
1876
+ try {
1877
+ writeKiroModelRecords(instanceDir, records);
1878
+ }
1879
+ catch { /* the next launch decides again; at worst it says so again */ }
1880
+ }
1881
+ /** #2648 steps 2 and 4: what a model error on this launch should do (records it); null before any launch. */
1882
+ onModelError() {
1883
+ const m = this.launchModels;
1884
+ if (!m)
1885
+ return null;
1886
+ return decideKiroModelError(m.instanceDir, m, readKiroKnownModels(getAgendHome())?.defaultModel ?? KIRO_FALLBACK_MODEL);
1887
+ }
1293
1888
  addLaunchWarning(message) {
1294
1889
  this.launchWarning = this.launchWarning ? `${this.launchWarning}\n${message}` : message;
1295
1890
  }
@@ -1656,7 +2251,7 @@ export class KiroBackend {
1656
2251
  // picker. Its choices range across credit multipliers and capability
1657
2252
  // tiers, so this must be a human decision, never an automatic Enter or
1658
2253
  // Escape. The ordinary /model menu alone is intentionally not matched.
1659
- pattern: /^\s*Select model \(type to search\):\s*$/m,
2254
+ pattern: KIRO_MODEL_PICKER_HEADER,
1660
2255
  keys: [],
1661
2256
  description: "Kiro model unavailable — choose a replacement model in the instance pane",
1662
2257
  blocksDelivery: true,
@@ -1668,7 +2263,7 @@ export class KiroBackend {
1668
2263
  // Any active /model picker owns stdin, even if a future Kiro release
1669
2264
  // changes the exact warning, credit wording or row layout. Keep this
1670
2265
  // after the verified-outage case: it never labels /model as an outage.
1671
- pattern: /^\s*Select model \(type to search\):\s*$/m,
2266
+ pattern: KIRO_MODEL_PICKER_HEADER,
1672
2267
  keys: [],
1673
2268
  description: "Kiro model picker — manual selection required",
1674
2269
  blocksDelivery: true,
@@ -1676,14 +2271,94 @@ export class KiroBackend {
1676
2271
  inputBlocked: true,
1677
2272
  isActive: kiroModelPickerAtTail,
1678
2273
  },
2274
+ {
2275
+ // #2587: an MCP server's input request (kiro-cli 2.34.0 V3) owns stdin until the person answers or cancels it.
2276
+ pattern: KIRO_MCP_INPUT_HEADER,
2277
+ keys: [],
2278
+ description: "Kiro MCP server is asking for input — answer it in the instance pane",
2279
+ blocksDelivery: true,
2280
+ holdOnly: true,
2281
+ inputBlocked: true,
2282
+ isActive: kiroMcpInputRequestActive,
2283
+ requestIdentity: pane => readKiroMcpInputCard(pane)?.identity ?? null,
2284
+ remoteCard: readKiroMcpInputCard,
2285
+ },
2286
+ {
2287
+ // #2592: the agent's question to the user owns stdin until the person answers or cancels it.
2288
+ pattern: KIRO_QUESTION_HEADER,
2289
+ keys: [],
2290
+ description: "Kiro agent is asking a question — answer it in the instance pane",
2291
+ blocksDelivery: true,
2292
+ holdOnly: true,
2293
+ inputBlocked: true,
2294
+ isActive: kiroQuestionCardActive,
2295
+ requestIdentity: pane => readKiroQuestionCard(pane)?.identity ?? null,
2296
+ remoteCard: readKiroQuestionCard,
2297
+ },
1679
2298
  ...kiroTrustPromptDialogs(this.activeTrustAll),
1680
2299
  ...kiroWorkspaceTrustDialogs(this.activeTrustAll),
2300
+ {
2301
+ // #2682: an editor, a pager, a `!` command or a focused /kvim owns the pane: a paste and an Enter would go into
2302
+ // it. Last, so every dialog above keeps its own words; held for the person, never keyed.
2303
+ pattern: KIRO_TAKEOVER_KEY,
2304
+ keys: [],
2305
+ description: "Kiro's terminal is in use by another program (an editor, a pager or a ! command) — finish or close it in the instance pane",
2306
+ blocksDelivery: true,
2307
+ holdOnly: true,
2308
+ inputBlocked: true,
2309
+ isActive: pane => kiroTakeoverActive(pane, this.activeUi),
2310
+ },
1681
2311
  ];
1682
2312
  }
2313
+ /**
2314
+ * #2682: kiro's own input row, read positively, before an idle delivery. TUI and V3: the composer reads "idle", or
2315
+ * "text" (a draft, as before); busy, another mode (a goal, the tray being edited) or no composer at all — an unknown
2316
+ * full-screen program, a blank frame — is not a screen to type into. Legacy: its prompt row is the bottom row (`4% !>`,
2317
+ * or `4% >` without trust-all, live 2.34.0), not running a `!` command. Anything else: the delivery waits.
2318
+ */
2319
+ /** #2682: the busy composer's input reaches kiro's own turn (kiroBusyTurnTakesInput), not a `!` command's PTY. */
2320
+ busyTurnTakesInput(pane) {
2321
+ return this.activeUi !== "legacy" && kiroBusyTurnTakesInput(pane);
2322
+ }
2323
+ idleComposerReady(pane) {
2324
+ if (this.activeUi === "legacy") {
2325
+ const prompt = kiroLegacyPromptAtBottom(pane.split("\n").map(row => row.replace(/\s+$/, "")));
2326
+ return prompt !== null && !prompt.startsWith("!");
2327
+ }
2328
+ const mode = readKiroSteerComposer(pane);
2329
+ return mode === "idle" || mode === "text";
2330
+ }
1683
2331
  getContextUsage() {
1684
2332
  return null;
1685
2333
  }
1686
2334
  logLaunch() { return this.builtLaunch ? { ...this.builtLaunch } : null; }
2335
+ /**
2336
+ * #2598 B1: the store is the launch's engine's (v1 = the classic sqlite store, v2 = the TUI's session files), and the
2337
+ * conversation is the one the launch resumed by id. A fresh or legacy-mode launch owns no id yet.
2338
+ */
2339
+ transcriptLaunch() {
2340
+ const built = this.builtLaunch, plan = this.activePlan;
2341
+ if (!built)
2342
+ return null;
2343
+ return { engine: built.engine, sessionId: plan?.mode === "resume" && plan.id ? plan.id : null };
2344
+ }
2345
+ /**
2346
+ * M-2025: `/session-id` and `/title` exist on tui and V3 only. Legacy keeps
2347
+ * the tmux window name and the sqlite lookup.
2348
+ */
2349
+ sessionProof() {
2350
+ const ui = this.activeUi;
2351
+ if (ui !== "tui" && ui !== "v3")
2352
+ return null;
2353
+ const pinned = this.transcriptLaunch()?.sessionId ?? null;
2354
+ return {
2355
+ pinned,
2356
+ sessionIdCommand: KIRO_SESSION_ID_COMMAND,
2357
+ titleCommand: this.activeInstance ? kiroTitleCommand(this.activeInstance) : null,
2358
+ readComposer: readKiroCommandComposer,
2359
+ readSessionIdAlert: readKiroSessionIdAlert,
2360
+ };
2361
+ }
1687
2362
  getSessionId() {
1688
2363
  // Kiro manages sessions internally via SQLite keyed by working directory.
1689
2364
  // No external session ID needed — --resume handles it automatically.
@@ -1709,7 +2384,7 @@ export class KiroBackend {
1709
2384
  */
1710
2385
  readModelsPayload() {
1711
2386
  try {
1712
- const out = execFileSync(this.binaryPath, ["chat", "--list-models", "--format", "json"], { encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
2387
+ const out = execFileSync(this.binaryPath, ["chat", "--list-models", "--format", "json"], { encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"], env: kiroChildEnv() });
1713
2388
  const parsed = JSON.parse(out);
1714
2389
  const arr = Array.isArray(parsed) ? parsed : (parsed?.models ?? []);
1715
2390
  const models = arr.map((m) => {