@murphai/murph 0.1.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 (301) hide show
  1. package/CHANGELOG.md +2009 -0
  2. package/LICENSE +674 -0
  3. package/README.md +97 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/assistant/automation/run-loop.d.ts +21 -0
  6. package/dist/assistant/automation/run-loop.d.ts.map +1 -0
  7. package/dist/assistant/automation/run-loop.js +31 -0
  8. package/dist/assistant/automation/run-loop.js.map +1 -0
  9. package/dist/assistant/automation.d.ts +10 -0
  10. package/dist/assistant/automation.d.ts.map +1 -0
  11. package/dist/assistant/automation.js +5 -0
  12. package/dist/assistant/automation.js.map +1 -0
  13. package/dist/assistant/cron.d.ts +19 -0
  14. package/dist/assistant/cron.d.ts.map +1 -0
  15. package/dist/assistant/cron.js +59 -0
  16. package/dist/assistant/cron.js.map +1 -0
  17. package/dist/assistant/doctor-security.d.ts +15 -0
  18. package/dist/assistant/doctor-security.d.ts.map +1 -0
  19. package/dist/assistant/doctor-security.js +172 -0
  20. package/dist/assistant/doctor-security.js.map +1 -0
  21. package/dist/assistant/doctor.d.ts +5 -0
  22. package/dist/assistant/doctor.d.ts.map +1 -0
  23. package/dist/assistant/doctor.js +527 -0
  24. package/dist/assistant/doctor.js.map +1 -0
  25. package/dist/assistant/outbox.d.ts +19 -0
  26. package/dist/assistant/outbox.d.ts.map +1 -0
  27. package/dist/assistant/outbox.js +28 -0
  28. package/dist/assistant/outbox.js.map +1 -0
  29. package/dist/assistant/provider-catalog.d.ts +61 -0
  30. package/dist/assistant/provider-catalog.d.ts.map +1 -0
  31. package/dist/assistant/provider-catalog.js +205 -0
  32. package/dist/assistant/provider-catalog.js.map +1 -0
  33. package/dist/assistant/service.d.ts +85 -0
  34. package/dist/assistant/service.d.ts.map +1 -0
  35. package/dist/assistant/service.js +26 -0
  36. package/dist/assistant/service.js.map +1 -0
  37. package/dist/assistant/status.d.ts +9 -0
  38. package/dist/assistant/status.d.ts.map +1 -0
  39. package/dist/assistant/status.js +16 -0
  40. package/dist/assistant/status.js.map +1 -0
  41. package/dist/assistant/stop.d.ts +20 -0
  42. package/dist/assistant/stop.d.ts.map +1 -0
  43. package/dist/assistant/stop.js +142 -0
  44. package/dist/assistant/stop.js.map +1 -0
  45. package/dist/assistant/store.d.ts +6 -0
  46. package/dist/assistant/store.d.ts.map +1 -0
  47. package/dist/assistant/store.js +21 -0
  48. package/dist/assistant/store.js.map +1 -0
  49. package/dist/assistant/ui/ink.d.ts +247 -0
  50. package/dist/assistant/ui/ink.d.ts.map +1 -0
  51. package/dist/assistant/ui/ink.js +2417 -0
  52. package/dist/assistant/ui/ink.js.map +1 -0
  53. package/dist/assistant/ui/theme.d.ts +64 -0
  54. package/dist/assistant/ui/theme.d.ts.map +1 -0
  55. package/dist/assistant/ui/theme.js +180 -0
  56. package/dist/assistant/ui/theme.js.map +1 -0
  57. package/dist/assistant/ui/view-model.d.ts +89 -0
  58. package/dist/assistant/ui/view-model.d.ts.map +1 -0
  59. package/dist/assistant/ui/view-model.js +298 -0
  60. package/dist/assistant/ui/view-model.js.map +1 -0
  61. package/dist/assistant-chat-ink.d.ts +2 -0
  62. package/dist/assistant-chat-ink.d.ts.map +1 -0
  63. package/dist/assistant-chat-ink.js +2 -0
  64. package/dist/assistant-chat-ink.js.map +1 -0
  65. package/dist/assistant-daemon-client.d.ts +81 -0
  66. package/dist/assistant-daemon-client.d.ts.map +1 -0
  67. package/dist/assistant-daemon-client.js +473 -0
  68. package/dist/assistant-daemon-client.js.map +1 -0
  69. package/dist/assistant-runtime.d.ts +25 -0
  70. package/dist/assistant-runtime.d.ts.map +1 -0
  71. package/dist/assistant-runtime.js +17 -0
  72. package/dist/assistant-runtime.js.map +1 -0
  73. package/dist/bin.d.ts +3 -0
  74. package/dist/bin.d.ts.map +1 -0
  75. package/dist/bin.js +7 -0
  76. package/dist/bin.js.map +1 -0
  77. package/dist/cli-entry.d.ts +10 -0
  78. package/dist/cli-entry.d.ts.map +1 -0
  79. package/dist/cli-entry.js +127 -0
  80. package/dist/cli-entry.js.map +1 -0
  81. package/dist/commands/assistant.d.ts +5 -0
  82. package/dist/commands/assistant.d.ts.map +1 -0
  83. package/dist/commands/assistant.js +1663 -0
  84. package/dist/commands/assistant.js.map +1 -0
  85. package/dist/commands/audit-command-helpers.d.ts +15 -0
  86. package/dist/commands/audit-command-helpers.d.ts.map +1 -0
  87. package/dist/commands/audit-command-helpers.js +24 -0
  88. package/dist/commands/audit-command-helpers.js.map +1 -0
  89. package/dist/commands/audit.d.ts +4 -0
  90. package/dist/commands/audit.d.ts.map +1 -0
  91. package/dist/commands/audit.js +107 -0
  92. package/dist/commands/audit.js.map +1 -0
  93. package/dist/commands/device.d.ts +4 -0
  94. package/dist/commands/device.d.ts.map +1 -0
  95. package/dist/commands/device.js +177 -0
  96. package/dist/commands/device.js.map +1 -0
  97. package/dist/commands/document.d.ts +4 -0
  98. package/dist/commands/document.d.ts.map +1 -0
  99. package/dist/commands/document.js +117 -0
  100. package/dist/commands/document.js.map +1 -0
  101. package/dist/commands/event.d.ts +4 -0
  102. package/dist/commands/event.d.ts.map +1 -0
  103. package/dist/commands/event.js +136 -0
  104. package/dist/commands/event.js.map +1 -0
  105. package/dist/commands/experiment.d.ts +4 -0
  106. package/dist/commands/experiment.d.ts.map +1 -0
  107. package/dist/commands/experiment.js +140 -0
  108. package/dist/commands/experiment.js.map +1 -0
  109. package/dist/commands/export-intake-read-helpers.d.ts +150 -0
  110. package/dist/commands/export-intake-read-helpers.d.ts.map +1 -0
  111. package/dist/commands/export-intake-read-helpers.js +328 -0
  112. package/dist/commands/export-intake-read-helpers.js.map +1 -0
  113. package/dist/commands/export.d.ts +4 -0
  114. package/dist/commands/export.d.ts.map +1 -0
  115. package/dist/commands/export.js +179 -0
  116. package/dist/commands/export.js.map +1 -0
  117. package/dist/commands/food.d.ts +4 -0
  118. package/dist/commands/food.d.ts.map +1 -0
  119. package/dist/commands/food.js +190 -0
  120. package/dist/commands/food.js.map +1 -0
  121. package/dist/commands/health-command-factory.d.ts +230 -0
  122. package/dist/commands/health-command-factory.d.ts.map +1 -0
  123. package/dist/commands/health-command-factory.js +551 -0
  124. package/dist/commands/health-command-factory.js.map +1 -0
  125. package/dist/commands/health-entity-command-registry.d.ts +27 -0
  126. package/dist/commands/health-entity-command-registry.d.ts.map +1 -0
  127. package/dist/commands/health-entity-command-registry.js +84 -0
  128. package/dist/commands/health-entity-command-registry.js.map +1 -0
  129. package/dist/commands/inbox.d.ts +5 -0
  130. package/dist/commands/inbox.d.ts.map +1 -0
  131. package/dist/commands/inbox.js +841 -0
  132. package/dist/commands/inbox.js.map +1 -0
  133. package/dist/commands/intake.d.ts +4 -0
  134. package/dist/commands/intake.d.ts.map +1 -0
  135. package/dist/commands/intake.js +175 -0
  136. package/dist/commands/intake.js.map +1 -0
  137. package/dist/commands/intervention.d.ts +4 -0
  138. package/dist/commands/intervention.d.ts.map +1 -0
  139. package/dist/commands/intervention.js +122 -0
  140. package/dist/commands/intervention.js.map +1 -0
  141. package/dist/commands/journal.d.ts +12 -0
  142. package/dist/commands/journal.d.ts.map +1 -0
  143. package/dist/commands/journal.js +186 -0
  144. package/dist/commands/journal.js.map +1 -0
  145. package/dist/commands/meal.d.ts +4 -0
  146. package/dist/commands/meal.d.ts.map +1 -0
  147. package/dist/commands/meal.js +123 -0
  148. package/dist/commands/meal.js.map +1 -0
  149. package/dist/commands/profile.d.ts +4 -0
  150. package/dist/commands/profile.d.ts.map +1 -0
  151. package/dist/commands/profile.js +62 -0
  152. package/dist/commands/profile.js.map +1 -0
  153. package/dist/commands/protocol.d.ts +4 -0
  154. package/dist/commands/protocol.d.ts.map +1 -0
  155. package/dist/commands/protocol.js +79 -0
  156. package/dist/commands/protocol.js.map +1 -0
  157. package/dist/commands/provider.d.ts +4 -0
  158. package/dist/commands/provider.d.ts.map +1 -0
  159. package/dist/commands/provider.js +115 -0
  160. package/dist/commands/provider.js.map +1 -0
  161. package/dist/commands/read.d.ts +4 -0
  162. package/dist/commands/read.d.ts.map +1 -0
  163. package/dist/commands/read.js +55 -0
  164. package/dist/commands/read.js.map +1 -0
  165. package/dist/commands/recipe.d.ts +4 -0
  166. package/dist/commands/recipe.d.ts.map +1 -0
  167. package/dist/commands/recipe.js +116 -0
  168. package/dist/commands/recipe.js.map +1 -0
  169. package/dist/commands/record-mutation-command-helpers.d.ts +196 -0
  170. package/dist/commands/record-mutation-command-helpers.d.ts.map +1 -0
  171. package/dist/commands/record-mutation-command-helpers.js +150 -0
  172. package/dist/commands/record-mutation-command-helpers.js.map +1 -0
  173. package/dist/commands/research.d.ts +3 -0
  174. package/dist/commands/research.d.ts.map +1 -0
  175. package/dist/commands/research.js +104 -0
  176. package/dist/commands/research.js.map +1 -0
  177. package/dist/commands/sample-batch-command-helpers.d.ts +24 -0
  178. package/dist/commands/sample-batch-command-helpers.d.ts.map +1 -0
  179. package/dist/commands/sample-batch-command-helpers.js +99 -0
  180. package/dist/commands/sample-batch-command-helpers.js.map +1 -0
  181. package/dist/commands/sample-import-command-helpers.d.ts +24 -0
  182. package/dist/commands/sample-import-command-helpers.d.ts.map +1 -0
  183. package/dist/commands/sample-import-command-helpers.js +49 -0
  184. package/dist/commands/sample-import-command-helpers.js.map +1 -0
  185. package/dist/commands/sample-query-command-helpers.d.ts +11 -0
  186. package/dist/commands/sample-query-command-helpers.d.ts.map +1 -0
  187. package/dist/commands/sample-query-command-helpers.js +26 -0
  188. package/dist/commands/sample-query-command-helpers.js.map +1 -0
  189. package/dist/commands/samples.d.ts +4 -0
  190. package/dist/commands/samples.d.ts.map +1 -0
  191. package/dist/commands/samples.js +261 -0
  192. package/dist/commands/samples.js.map +1 -0
  193. package/dist/commands/search.d.ts +4 -0
  194. package/dist/commands/search.d.ts.map +1 -0
  195. package/dist/commands/search.js +295 -0
  196. package/dist/commands/search.js.map +1 -0
  197. package/dist/commands/supplement.d.ts +4 -0
  198. package/dist/commands/supplement.d.ts.map +1 -0
  199. package/dist/commands/supplement.js +338 -0
  200. package/dist/commands/supplement.js.map +1 -0
  201. package/dist/commands/vault.d.ts +4 -0
  202. package/dist/commands/vault.d.ts.map +1 -0
  203. package/dist/commands/vault.js +164 -0
  204. package/dist/commands/vault.js.map +1 -0
  205. package/dist/commands/workout.d.ts +4 -0
  206. package/dist/commands/workout.d.ts.map +1 -0
  207. package/dist/commands/workout.js +284 -0
  208. package/dist/commands/workout.js.map +1 -0
  209. package/dist/incur.generated.d.ts +2164 -0
  210. package/dist/incur.generated.d.ts.map +1 -0
  211. package/dist/incur.generated.js +2 -0
  212. package/dist/incur.generated.js.map +1 -0
  213. package/dist/index.d.ts +13 -0
  214. package/dist/index.d.ts.map +1 -0
  215. package/dist/index.js +14 -0
  216. package/dist/index.js.map +1 -0
  217. package/dist/research-cli-contracts.d.ts +22 -0
  218. package/dist/research-cli-contracts.d.ts.map +1 -0
  219. package/dist/research-cli-contracts.js +18 -0
  220. package/dist/research-cli-contracts.js.map +1 -0
  221. package/dist/research-runtime.d.ts +79 -0
  222. package/dist/research-runtime.d.ts.map +1 -0
  223. package/dist/research-runtime.js +351 -0
  224. package/dist/research-runtime.js.map +1 -0
  225. package/dist/run-terminal-logging.d.ts +12 -0
  226. package/dist/run-terminal-logging.d.ts.map +1 -0
  227. package/dist/run-terminal-logging.js +323 -0
  228. package/dist/run-terminal-logging.js.map +1 -0
  229. package/dist/setup-agentmail.d.ts +30 -0
  230. package/dist/setup-agentmail.d.ts.map +1 -0
  231. package/dist/setup-agentmail.js +136 -0
  232. package/dist/setup-agentmail.js.map +1 -0
  233. package/dist/setup-assistant-account.d.ts +29 -0
  234. package/dist/setup-assistant-account.d.ts.map +1 -0
  235. package/dist/setup-assistant-account.js +443 -0
  236. package/dist/setup-assistant-account.js.map +1 -0
  237. package/dist/setup-assistant.d.ts +34 -0
  238. package/dist/setup-assistant.d.ts.map +1 -0
  239. package/dist/setup-assistant.js +355 -0
  240. package/dist/setup-assistant.js.map +1 -0
  241. package/dist/setup-cli.d.ts +72 -0
  242. package/dist/setup-cli.d.ts.map +1 -0
  243. package/dist/setup-cli.js +387 -0
  244. package/dist/setup-cli.js.map +1 -0
  245. package/dist/setup-services/channels.d.ts +19 -0
  246. package/dist/setup-services/channels.d.ts.map +1 -0
  247. package/dist/setup-services/channels.js +721 -0
  248. package/dist/setup-services/channels.js.map +1 -0
  249. package/dist/setup-services/process.d.ts +18 -0
  250. package/dist/setup-services/process.d.ts.map +1 -0
  251. package/dist/setup-services/process.js +98 -0
  252. package/dist/setup-services/process.js.map +1 -0
  253. package/dist/setup-services/scheduled-updates.d.ts +9 -0
  254. package/dist/setup-services/scheduled-updates.d.ts.map +1 -0
  255. package/dist/setup-services/scheduled-updates.js +64 -0
  256. package/dist/setup-services/scheduled-updates.js.map +1 -0
  257. package/dist/setup-services/shell.d.ts +18 -0
  258. package/dist/setup-services/shell.d.ts.map +1 -0
  259. package/dist/setup-services/shell.js +447 -0
  260. package/dist/setup-services/shell.js.map +1 -0
  261. package/dist/setup-services/steps.d.ts +39 -0
  262. package/dist/setup-services/steps.d.ts.map +1 -0
  263. package/dist/setup-services/steps.js +86 -0
  264. package/dist/setup-services/steps.js.map +1 -0
  265. package/dist/setup-services/toolchain.d.ts +46 -0
  266. package/dist/setup-services/toolchain.d.ts.map +1 -0
  267. package/dist/setup-services/toolchain.js +232 -0
  268. package/dist/setup-services/toolchain.js.map +1 -0
  269. package/dist/setup-services.d.ts +44 -0
  270. package/dist/setup-services.d.ts.map +1 -0
  271. package/dist/setup-services.js +739 -0
  272. package/dist/setup-services.js.map +1 -0
  273. package/dist/setup-wizard.d.ts +101 -0
  274. package/dist/setup-wizard.d.ts.map +1 -0
  275. package/dist/setup-wizard.js +1458 -0
  276. package/dist/setup-wizard.js.map +1 -0
  277. package/dist/usecases/intervention.d.ts +63 -0
  278. package/dist/usecases/intervention.d.ts.map +1 -0
  279. package/dist/usecases/intervention.js +205 -0
  280. package/dist/usecases/intervention.js.map +1 -0
  281. package/dist/usecases/text-duration.d.ts +4 -0
  282. package/dist/usecases/text-duration.d.ts.map +1 -0
  283. package/dist/usecases/text-duration.js +63 -0
  284. package/dist/usecases/text-duration.js.map +1 -0
  285. package/dist/usecases/workout-format.d.ts +139 -0
  286. package/dist/usecases/workout-format.d.ts.map +1 -0
  287. package/dist/usecases/workout-format.js +445 -0
  288. package/dist/usecases/workout-format.js.map +1 -0
  289. package/dist/usecases/workout.d.ts +94 -0
  290. package/dist/usecases/workout.d.ts.map +1 -0
  291. package/dist/usecases/workout.js +411 -0
  292. package/dist/usecases/workout.js.map +1 -0
  293. package/dist/vault-cli-command-manifest.d.ts +562 -0
  294. package/dist/vault-cli-command-manifest.d.ts.map +1 -0
  295. package/dist/vault-cli-command-manifest.js +759 -0
  296. package/dist/vault-cli-command-manifest.js.map +1 -0
  297. package/dist/vault-cli.d.ts +6 -0
  298. package/dist/vault-cli.d.ts.map +1 -0
  299. package/dist/vault-cli.js +38 -0
  300. package/dist/vault-cli.js.map +1 -0
  301. package/package.json +85 -0
@@ -0,0 +1,2417 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import tty from 'node:tty';
4
+ import { pathToFileURL } from 'node:url';
5
+ import * as React from 'react';
6
+ import { Box, Static, Text, render, useApp, useInput, useStdout, } from 'ink';
7
+ import { assistantChatResultSchema, } from '@murphai/assistant-core/assistant-cli-contracts';
8
+ import { discoverAssistantProviderModels, resolveAssistantCatalogReasoningOptions, resolveAssistantModelCatalog, } from '../provider-catalog.js';
9
+ import { resolveCodexDisplayOptions } from '@murphai/assistant-core/assistant-codex';
10
+ import { buildAssistantProviderDefaultsPatch, resolveAssistantOperatorDefaults, resolveAssistantProviderDefaults, saveAssistantOperatorDefaultsPatch, } from '@murphai/assistant-core/operator-config';
11
+ import { openAssistantConversation, sendAssistantMessage, updateAssistantSessionOptions, } from '../service.js';
12
+ import { extractRecoveredAssistantSession, isAssistantProviderConnectionLostError, isAssistantProviderInterruptedError, } from '@murphai/assistant-core/assistant-provider';
13
+ import { appendAssistantTranscriptEntries, isAssistantSessionNotFoundError, listAssistantTranscriptEntries, redactAssistantDisplayPath, } from '../store.js';
14
+ import { normalizeNullableString } from '@murphai/assistant-core/assistant-runtime';
15
+ import { redactAssistantSessionForDisplay } from '@murphai/assistant-core/assistant-runtime';
16
+ import { CHAT_COMPOSER_HINT, CHAT_SLASH_COMMANDS, CHAT_STARTER_SUGGESTIONS, applyProviderProgressEventToEntries, finalizePendingInkChatTraces, findAssistantModelOptionIndex, findAssistantReasoningOptionIndex, formatSessionBinding, applyInkChatTraceUpdates, getMatchingSlashCommands, resolveChatMetadataBadges, resolveChatSubmitAction, shouldShowChatComposerGuidance, shouldClearComposerForSubmitAction, seedChatEntries, } from './view-model.js';
17
+ import { LIGHT_ASSISTANT_INK_THEME, captureAssistantInkThemeBaseline, resolveAssistantInkThemeForOpenChat, } from './theme.js';
18
+ const AssistantInkThemeContext = React.createContext(LIGHT_ASSISTANT_INK_THEME);
19
+ const StaticTranscript = Static;
20
+ const COMPOSER_WORD_SEPARATORS = "`~!@#$%^&*()-=+[{]}\\\\|;:'\\\",.<>/?";
21
+ const MODIFIED_RETURN_SEQUENCE = /^\u001b?\[27;(\d+);13~$/u;
22
+ const RAW_ARROW_SEQUENCE = /^\u001b?(?:\[(?:(\d+;)?(\d+))?([ABCD])|O([ABCD]))$/u;
23
+ const QUEUED_FOLLOW_UP_SHORTCUT_HINT = '⌥ + ↑ edit last queued message';
24
+ const MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH = 88;
25
+ const ASSISTANT_INK_THEME_REFRESH_INTERVAL_MS = 2_000;
26
+ function useAssistantInkTheme() {
27
+ return React.useContext(AssistantInkThemeContext);
28
+ }
29
+ const BUSY_INDICATOR_CHARACTER = '•';
30
+ const ASSISTANT_CHAT_VIEW_PADDING_X = 1;
31
+ const ASSISTANT_PLAIN_TEXT_WRAP_SLACK = 1;
32
+ const ASSISTANT_INK_TTY_PATH = process.platform === 'win32' ? 'CONIN$' : '/dev/tty';
33
+ export function resolveChromePanelBoxProps(props) {
34
+ const boxProps = {
35
+ flexDirection: 'column',
36
+ marginBottom: props.marginBottom ?? 1,
37
+ paddingX: props.paddingX ??
38
+ (typeof props.backgroundColor === 'string' && props.backgroundColor.length > 0
39
+ ? 1
40
+ : 0),
41
+ paddingY: props.paddingY ?? 0,
42
+ width: props.width ?? '100%',
43
+ };
44
+ if (typeof props.backgroundColor === 'string' && props.backgroundColor.length > 0) {
45
+ boxProps.backgroundColor = props.backgroundColor;
46
+ }
47
+ return boxProps;
48
+ }
49
+ export function supportsAssistantInkRawMode(stdin) {
50
+ return Boolean(stdin?.isTTY && typeof stdin.setRawMode === 'function');
51
+ }
52
+ export function resolveAssistantInkInputAdapter(input = {}) {
53
+ const stdin = input.stdin ?? process.stdin;
54
+ if (supportsAssistantInkRawMode(stdin)) {
55
+ return {
56
+ close: () => { },
57
+ source: 'stdin',
58
+ stdin,
59
+ };
60
+ }
61
+ const ttyPath = input.ttyPath ?? ASSISTANT_INK_TTY_PATH;
62
+ const openTtyFd = input.openTtyFd ??
63
+ ((pathToOpen, flags) => fs.openSync(pathToOpen, flags));
64
+ const createTtyReadStream = input.createTtyReadStream ??
65
+ ((fd) => new tty.ReadStream(fd));
66
+ let fd = null;
67
+ let closed = false;
68
+ const closeFallbackFd = () => {
69
+ if (fd === null || closed) {
70
+ return;
71
+ }
72
+ closed = true;
73
+ try {
74
+ fs.closeSync(fd);
75
+ }
76
+ catch { }
77
+ };
78
+ try {
79
+ fd = openTtyFd(ttyPath, 'r');
80
+ const ttyInput = createTtyReadStream(fd);
81
+ if (!supportsAssistantInkRawMode(ttyInput)) {
82
+ ttyInput.destroy?.();
83
+ closeFallbackFd();
84
+ return {
85
+ close: () => { },
86
+ source: 'unsupported',
87
+ stdin: null,
88
+ };
89
+ }
90
+ return {
91
+ close: () => {
92
+ ttyInput.destroy?.();
93
+ closeFallbackFd();
94
+ },
95
+ source: 'tty',
96
+ stdin: ttyInput,
97
+ };
98
+ }
99
+ catch {
100
+ closeFallbackFd();
101
+ return {
102
+ close: () => { },
103
+ source: 'unsupported',
104
+ stdin: null,
105
+ };
106
+ }
107
+ }
108
+ const ChromePanel = React.memo(function ChromePanel(props) {
109
+ const createElement = React.createElement;
110
+ return createElement(Box, resolveChromePanelBoxProps(props), props.children);
111
+ });
112
+ const BusySpinner = React.memo(function BusySpinner(input) {
113
+ const createElement = React.createElement;
114
+ const theme = useAssistantInkTheme();
115
+ return createElement(Text, {
116
+ color: input.color ?? theme.accentColor,
117
+ }, BUSY_INDICATOR_CHARACTER);
118
+ });
119
+ export function resolveMessageRoleLabel(kind) {
120
+ if (kind === 'error') {
121
+ return 'error';
122
+ }
123
+ return null;
124
+ }
125
+ const MessageRoleLabel = React.memo(function MessageRoleLabel(input) {
126
+ const createElement = React.createElement;
127
+ const theme = useAssistantInkTheme();
128
+ const label = resolveMessageRoleLabel(input.kind);
129
+ if (!label) {
130
+ return null;
131
+ }
132
+ return createElement(Box, {
133
+ marginBottom: 1,
134
+ }, createElement(Text, {
135
+ bold: true,
136
+ color: theme.errorColor,
137
+ }, label));
138
+ });
139
+ export function renderWrappedTextBlock(input) {
140
+ const createElement = React.createElement;
141
+ return createElement(Box, {
142
+ flexDirection: 'column',
143
+ width: '100%',
144
+ }, createElement(Text, {
145
+ color: input.color,
146
+ dimColor: input.dimColor,
147
+ wrap: 'wrap',
148
+ }, input.children));
149
+ }
150
+ export function wrapAssistantPlainText(input, columns) {
151
+ return input
152
+ .replaceAll('\r\n', '\n')
153
+ .split('\n')
154
+ .map((line) => wrapAssistantPlainTextLine(line, columns))
155
+ .join('\n');
156
+ }
157
+ function wrapAssistantPlainTextLine(input, columns) {
158
+ if (input.length === 0 || columns <= 0) {
159
+ return input;
160
+ }
161
+ const leadingWhitespace = input.match(/^\s*/u)?.[0] ?? '';
162
+ const content = input.slice(leadingWhitespace.length);
163
+ if (content.length === 0) {
164
+ return input;
165
+ }
166
+ const tokens = content.match(/\S+|\s+/gu) ?? [content];
167
+ const lines = [];
168
+ let currentLine = leadingWhitespace;
169
+ let currentWidth = leadingWhitespace.length;
170
+ let pendingWhitespace = '';
171
+ for (const token of tokens) {
172
+ if (/^\s+$/u.test(token)) {
173
+ pendingWhitespace += token;
174
+ continue;
175
+ }
176
+ const spacer = currentWidth > leadingWhitespace.length
177
+ ? pendingWhitespace.length > 0
178
+ ? pendingWhitespace
179
+ : ' '
180
+ : '';
181
+ const candidateWidth = currentWidth + spacer.length + token.length;
182
+ if (currentWidth > leadingWhitespace.length && candidateWidth > columns) {
183
+ lines.push(currentLine);
184
+ currentLine = `${leadingWhitespace}${token}`;
185
+ currentWidth = leadingWhitespace.length + token.length;
186
+ pendingWhitespace = '';
187
+ continue;
188
+ }
189
+ if (spacer.length > 0) {
190
+ currentLine += spacer;
191
+ currentWidth += spacer.length;
192
+ }
193
+ currentLine += token;
194
+ currentWidth += token.length;
195
+ pendingWhitespace = '';
196
+ }
197
+ lines.push(currentLine);
198
+ return lines.join('\n');
199
+ }
200
+ function resolveAssistantTerminalColumns(columns) {
201
+ return typeof columns === 'number' && Number.isFinite(columns)
202
+ ? Math.max(1, Math.floor(columns))
203
+ : 80;
204
+ }
205
+ export function resolveAssistantChatViewportWidth(columns) {
206
+ return Math.max(1, resolveAssistantTerminalColumns(columns) - ASSISTANT_CHAT_VIEW_PADDING_X * 2);
207
+ }
208
+ export function resolveAssistantPlainTextWrapColumns(columns) {
209
+ return Math.max(1, resolveAssistantChatViewportWidth(columns) - ASSISTANT_PLAIN_TEXT_WRAP_SLACK);
210
+ }
211
+ const WrappedTextBlock = React.memo(function WrappedTextBlock(input) {
212
+ return renderWrappedTextBlock(input);
213
+ });
214
+ export function renderWrappedPlainTextBlock(input) {
215
+ const createElement = React.createElement;
216
+ const lines = wrapAssistantPlainText(input.text, input.columns).split('\n');
217
+ return createElement(Box, {
218
+ flexDirection: 'column',
219
+ width: '100%',
220
+ }, ...lines.map((line, index) => createElement(Text, {
221
+ color: input.color,
222
+ dimColor: input.dimColor,
223
+ key: `wrapped-plain-text:${index}`,
224
+ }, line.length > 0 ? line : ' ')));
225
+ }
226
+ export function resolveAssistantTurnErrorPresentation(input) {
227
+ const errorText = input.error instanceof Error ? input.error.message : String(input.error);
228
+ const connectionLost = isAssistantProviderConnectionLostError(input.error);
229
+ const missingSession = isAssistantSessionNotFoundError(input.error);
230
+ const queuedFollowUpSuffix = input.restoredQueuedPromptCount > 0
231
+ ? ' Queued follow-ups are back in the composer.'
232
+ : '';
233
+ return {
234
+ entry: {
235
+ kind: 'error',
236
+ text: errorText,
237
+ },
238
+ persistTranscriptError: !missingSession,
239
+ status: connectionLost
240
+ ? {
241
+ kind: 'error',
242
+ text: `The assistant lost its provider connection. Restore connectivity, then keep chatting to resume.${queuedFollowUpSuffix}`,
243
+ }
244
+ : missingSession
245
+ ? {
246
+ kind: 'error',
247
+ text: `The local assistant session record is missing. Check the current vault/default vault or start a new chat.${queuedFollowUpSuffix}`,
248
+ }
249
+ : {
250
+ kind: 'error',
251
+ text: `The assistant hit an error. Fix it or keep chatting.${queuedFollowUpSuffix}`,
252
+ },
253
+ };
254
+ }
255
+ const WrappedPlainTextBlock = React.memo(function WrappedPlainTextBlock(input) {
256
+ return renderWrappedPlainTextBlock(input);
257
+ });
258
+ const FooterBadge = React.memo(function FooterBadge(input) {
259
+ const createElement = React.createElement;
260
+ const theme = useAssistantInkTheme();
261
+ const backgroundColor = input.badge.key === 'model' ? theme.accentColor : theme.footerBadgeBackground;
262
+ const color = input.badge.key === 'model'
263
+ ? theme.composerCursorTextColor
264
+ : input.badge.key === 'vault'
265
+ ? theme.mutedColor
266
+ : theme.footerBadgeTextColor;
267
+ return createElement(Text, {
268
+ backgroundColor,
269
+ color,
270
+ }, formatFooterBadgeText(input.badge));
271
+ });
272
+ export function formatFooterBadgeText(badge) {
273
+ if (badge.key === 'model' || badge.key === 'reasoning') {
274
+ return ` ${badge.value} `;
275
+ }
276
+ return ` ${badge.label}: ${badge.value} `;
277
+ }
278
+ export function splitAssistantMarkdownLinks(input) {
279
+ const segments = [];
280
+ const markdownLinkPattern = /\[([^\]\n]+)\]\(([^)\s]+)\)/gu;
281
+ let lastIndex = 0;
282
+ for (const match of input.matchAll(markdownLinkPattern)) {
283
+ const matchedText = match[0];
284
+ const label = match[1];
285
+ const target = match[2];
286
+ const start = match.index ?? -1;
287
+ if (typeof matchedText !== 'string' ||
288
+ typeof label !== 'string' ||
289
+ typeof target !== 'string' ||
290
+ start < 0) {
291
+ continue;
292
+ }
293
+ if (start > lastIndex) {
294
+ segments.push({
295
+ kind: 'text',
296
+ text: input.slice(lastIndex, start),
297
+ });
298
+ }
299
+ segments.push({
300
+ kind: 'link',
301
+ label,
302
+ target,
303
+ });
304
+ lastIndex = start + matchedText.length;
305
+ }
306
+ if (lastIndex < input.length) {
307
+ segments.push({
308
+ kind: 'text',
309
+ text: input.slice(lastIndex),
310
+ });
311
+ }
312
+ return segments.length > 0
313
+ ? segments
314
+ : [
315
+ {
316
+ kind: 'text',
317
+ text: input,
318
+ },
319
+ ];
320
+ }
321
+ export function resolveAssistantHyperlinkTarget(target) {
322
+ if (/^(https?|mailto):/iu.test(target)) {
323
+ return target;
324
+ }
325
+ const fragmentIndex = target.indexOf('#');
326
+ const pathPart = fragmentIndex >= 0
327
+ ? target.slice(0, fragmentIndex)
328
+ : target;
329
+ const fragment = fragmentIndex >= 0
330
+ ? target.slice(fragmentIndex)
331
+ : '';
332
+ if (!path.isAbsolute(pathPart)) {
333
+ return null;
334
+ }
335
+ return `${pathToFileURL(pathPart).href}${fragment}`;
336
+ }
337
+ export function formatAssistantTerminalHyperlink(label, target) {
338
+ return `\u001B]8;;${target}\u0007${label}\u001B]8;;\u0007`;
339
+ }
340
+ export function supportsAssistantTerminalHyperlinks(input = {}) {
341
+ const env = input.env ?? process.env;
342
+ const isTTY = input.isTTY ?? process.stderr.isTTY ?? false;
343
+ if (!isTTY || env.CI === 'true') {
344
+ return false;
345
+ }
346
+ if (env.FORCE_HYPERLINK === '1') {
347
+ return true;
348
+ }
349
+ return Boolean(env.KITTY_WINDOW_ID ||
350
+ env.ITERM_SESSION_ID ||
351
+ env.WT_SESSION ||
352
+ env.WEZTERM_PANE ||
353
+ env.VSCODE_INJECTION ||
354
+ env.TERM_PROGRAM === 'Apple_Terminal' ||
355
+ env.TERM_PROGRAM === 'WarpTerminal' ||
356
+ env.TERM_PROGRAM === 'vscode');
357
+ }
358
+ export function renderAssistantMessageText(input) {
359
+ const createElement = React.createElement;
360
+ const theme = useAssistantInkTheme();
361
+ const { stdout } = useStdout();
362
+ const enableHyperlinks = supportsAssistantTerminalHyperlinks();
363
+ const segments = splitAssistantMarkdownLinks(input.text);
364
+ const plainTextOnly = segments.every((segment) => segment.kind === 'text');
365
+ if (plainTextOnly) {
366
+ return createElement(WrappedTextBlock, {}, wrapAssistantPlainText(input.text, resolveAssistantPlainTextWrapColumns(stdout?.columns)));
367
+ }
368
+ return createElement(WrappedTextBlock, {}, ...segments.map((segment, index) => {
369
+ if (segment.kind === 'text') {
370
+ return segment.text;
371
+ }
372
+ const hyperlinkTarget = resolveAssistantHyperlinkTarget(segment.target);
373
+ const displayedLabel = hyperlinkTarget && enableHyperlinks
374
+ ? formatAssistantTerminalHyperlink(segment.label, hyperlinkTarget)
375
+ : segment.label;
376
+ return createElement(Text, {
377
+ color: theme.accentColor,
378
+ key: `link:${index}:${segment.label}`,
379
+ underline: true,
380
+ }, displayedLabel);
381
+ }));
382
+ }
383
+ function resolveComposerModifiedReturnAction(input, key) {
384
+ const match = MODIFIED_RETURN_SEQUENCE.exec(input);
385
+ if (!match) {
386
+ return null;
387
+ }
388
+ const modifier = Math.max(0, Number.parseInt(match[1] ?? '1', 10) - 1);
389
+ const shift = key.shift || (modifier & 1) === 1;
390
+ if (!shift) {
391
+ return {
392
+ kind: 'submit',
393
+ mode: 'enter',
394
+ };
395
+ }
396
+ return {
397
+ kind: 'edit',
398
+ input: '\n',
399
+ key: {
400
+ ...key,
401
+ return: false,
402
+ shift: true,
403
+ },
404
+ };
405
+ }
406
+ export function normalizeAssistantInkArrowKey(input, key) {
407
+ if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
408
+ return key;
409
+ }
410
+ const match = RAW_ARROW_SEQUENCE.exec(input);
411
+ const direction = match?.[3] ?? match?.[4];
412
+ if (!direction) {
413
+ return key;
414
+ }
415
+ const modifier = Math.max(0, Number.parseInt(match?.[2] ?? '1', 10) - 1);
416
+ return {
417
+ ...key,
418
+ ctrl: key.ctrl || (modifier & 4) === 4,
419
+ downArrow: direction === 'B',
420
+ leftArrow: direction === 'D',
421
+ meta: key.meta || (modifier & 2) === 2,
422
+ rightArrow: direction === 'C',
423
+ shift: key.shift || (modifier & 1) === 1,
424
+ upArrow: direction === 'A',
425
+ };
426
+ }
427
+ export function mergeComposerDraftWithQueuedPrompts(draft, queuedPrompts) {
428
+ return [draft, ...queuedPrompts]
429
+ .filter((value) => value.trim().length > 0)
430
+ .join('\n\n');
431
+ }
432
+ export function resolveComposerTerminalAction(input, key) {
433
+ const normalizedKey = normalizeAssistantInkArrowKey(input, key);
434
+ const modifiedReturnAction = resolveComposerModifiedReturnAction(input, normalizedKey);
435
+ if (modifiedReturnAction) {
436
+ return modifiedReturnAction;
437
+ }
438
+ if ((input === '\u007f' || input === '\b') &&
439
+ !normalizedKey.ctrl &&
440
+ !normalizedKey.meta &&
441
+ !normalizedKey.shift &&
442
+ !normalizedKey.super &&
443
+ !normalizedKey.hyper) {
444
+ return {
445
+ kind: 'edit',
446
+ input: '',
447
+ key: {
448
+ ...normalizedKey,
449
+ backspace: true,
450
+ delete: false,
451
+ },
452
+ };
453
+ }
454
+ if (normalizedKey.meta && normalizedKey.upArrow) {
455
+ return {
456
+ kind: 'edit-last-queued',
457
+ };
458
+ }
459
+ if (normalizedKey.tab && !normalizedKey.shift) {
460
+ return {
461
+ kind: 'submit',
462
+ mode: 'tab',
463
+ };
464
+ }
465
+ if (normalizedKey.return) {
466
+ if (!normalizedKey.shift) {
467
+ return {
468
+ kind: 'submit',
469
+ mode: 'enter',
470
+ };
471
+ }
472
+ return {
473
+ kind: 'edit',
474
+ input: '\n',
475
+ key: {
476
+ ...normalizedKey,
477
+ return: false,
478
+ },
479
+ };
480
+ }
481
+ if (normalizedKey.delete) {
482
+ // Many terminals report the primary delete/backspace key as `delete`.
483
+ // Preserve an actual forward-delete path via Ctrl+D inside the editor helpers.
484
+ return {
485
+ kind: 'edit',
486
+ input,
487
+ key: {
488
+ ...normalizedKey,
489
+ backspace: true,
490
+ delete: false,
491
+ },
492
+ };
493
+ }
494
+ return {
495
+ kind: 'edit',
496
+ input,
497
+ key: normalizedKey,
498
+ };
499
+ }
500
+ export function formatQueuedFollowUpPreview(prompt) {
501
+ const normalized = prompt.trim().replace(/\s+/gu, ' ');
502
+ if (normalized.length <= MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH) {
503
+ return normalized;
504
+ }
505
+ const truncated = normalized
506
+ .slice(0, MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH - 1)
507
+ .trimEnd();
508
+ const boundary = truncated.lastIndexOf(' ');
509
+ const preview = boundary >= Math.floor(MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH / 2)
510
+ ? truncated.slice(0, boundary).trimEnd()
511
+ : truncated;
512
+ return `${preview}…`;
513
+ }
514
+ function ComposerInput(props) {
515
+ const createElement = React.createElement;
516
+ const theme = useAssistantInkTheme();
517
+ const [displayValue, setDisplayValue] = React.useState(props.value);
518
+ const [cursorOffset, setCursorOffset] = React.useState(props.value.length);
519
+ const valueRef = React.useRef(props.value);
520
+ const cursorOffsetRef = React.useRef(props.value.length);
521
+ const killBufferRef = React.useRef('');
522
+ const preferredColumnRef = React.useRef(null);
523
+ const lastPropValueRef = React.useRef(props.value);
524
+ // Keep a queue of locally emitted draft values so older controlled echoes
525
+ // cannot clobber a newer in-flight paste or mid-buffer edit.
526
+ const pendingControlledValuesRef = React.useRef([]);
527
+ const onChangeRef = React.useRef(props.onChange);
528
+ const onEditLastQueuedPromptRef = React.useRef(props.onEditLastQueuedPrompt);
529
+ const onSubmitRef = React.useRef(props.onSubmit);
530
+ const disabledRef = React.useRef(props.disabled);
531
+ onChangeRef.current = props.onChange;
532
+ onEditLastQueuedPromptRef.current = props.onEditLastQueuedPrompt;
533
+ onSubmitRef.current = props.onSubmit;
534
+ disabledRef.current = props.disabled;
535
+ React.useLayoutEffect(() => {
536
+ const syncResult = reconcileComposerControlledValue({
537
+ cursorOffset: cursorOffsetRef.current,
538
+ currentValue: valueRef.current,
539
+ nextControlledValue: props.value,
540
+ pendingValues: pendingControlledValuesRef.current,
541
+ previousControlledValue: lastPropValueRef.current,
542
+ });
543
+ lastPropValueRef.current = props.value;
544
+ pendingControlledValuesRef.current = syncResult.pendingValues;
545
+ valueRef.current = syncResult.nextValue;
546
+ setDisplayValue((previous) => previous === syncResult.nextValue ? previous : syncResult.nextValue);
547
+ if (syncResult.cursorOffset !== cursorOffsetRef.current) {
548
+ cursorOffsetRef.current = syncResult.cursorOffset;
549
+ setCursorOffset(syncResult.cursorOffset);
550
+ }
551
+ }, [props.value]);
552
+ const handleComposerInput = React.useCallback((input, key) => {
553
+ if (disabledRef.current) {
554
+ return;
555
+ }
556
+ if ((key.shift && key.tab) || (key.ctrl && input === 'c')) {
557
+ return;
558
+ }
559
+ const currentValue = valueRef.current;
560
+ const currentCursorOffset = cursorOffsetRef.current;
561
+ const action = resolveComposerTerminalAction(input, key);
562
+ if (action.kind === 'edit-last-queued') {
563
+ onEditLastQueuedPromptRef.current();
564
+ return;
565
+ }
566
+ if (action.kind === 'edit' &&
567
+ (action.key.upArrow || action.key.downArrow)) {
568
+ const verticalMovement = resolveComposerVerticalCursorMove({
569
+ cursorOffset: currentCursorOffset,
570
+ direction: action.key.upArrow ? 'up' : 'down',
571
+ preferredColumn: preferredColumnRef.current,
572
+ value: currentValue,
573
+ });
574
+ if (verticalMovement.cursorOffset !== currentCursorOffset) {
575
+ cursorOffsetRef.current = verticalMovement.cursorOffset;
576
+ preferredColumnRef.current = verticalMovement.preferredColumn;
577
+ setCursorOffset(verticalMovement.cursorOffset);
578
+ }
579
+ return;
580
+ }
581
+ if (action.kind === 'submit') {
582
+ if (onSubmitRef.current(currentValue, action.mode) === 'clear') {
583
+ valueRef.current = '';
584
+ pendingControlledValuesRef.current = enqueuePendingComposerValue(pendingControlledValuesRef.current, '');
585
+ cursorOffsetRef.current = 0;
586
+ killBufferRef.current = '';
587
+ preferredColumnRef.current = null;
588
+ setDisplayValue('');
589
+ setCursorOffset(0);
590
+ onChangeRef.current('');
591
+ }
592
+ return;
593
+ }
594
+ if (action.kind !== 'edit') {
595
+ return;
596
+ }
597
+ const editingResult = applyComposerEditingInput({
598
+ cursorOffset: currentCursorOffset,
599
+ killBuffer: killBufferRef.current,
600
+ value: currentValue,
601
+ }, action.input, action.key);
602
+ if (!editingResult.handled) {
603
+ return;
604
+ }
605
+ cursorOffsetRef.current = editingResult.cursorOffset;
606
+ killBufferRef.current = editingResult.killBuffer;
607
+ valueRef.current = editingResult.value;
608
+ preferredColumnRef.current = null;
609
+ if (editingResult.cursorOffset !== currentCursorOffset) {
610
+ setCursorOffset(editingResult.cursorOffset);
611
+ }
612
+ if (editingResult.value !== currentValue) {
613
+ pendingControlledValuesRef.current = enqueuePendingComposerValue(pendingControlledValuesRef.current, editingResult.value);
614
+ setDisplayValue(editingResult.value);
615
+ onChangeRef.current(editingResult.value);
616
+ }
617
+ }, []);
618
+ useInput(handleComposerInput, {
619
+ isActive: !props.disabled,
620
+ });
621
+ return createElement(Box, {
622
+ flexDirection: 'column',
623
+ flexGrow: 1,
624
+ flexShrink: 1,
625
+ }, renderComposerValue({
626
+ cursorOffset,
627
+ disabled: props.disabled,
628
+ placeholder: props.placeholder,
629
+ theme,
630
+ value: displayValue,
631
+ }));
632
+ }
633
+ const ChatHeader = React.memo(function ChatHeader(props) {
634
+ const createElement = React.createElement;
635
+ const theme = useAssistantInkTheme();
636
+ const terminalColumns = process.stderr.columns ?? 80;
637
+ const terminalRows = process.stderr.rows ?? 24;
638
+ const compactHeader = terminalColumns < 72 || terminalRows < 18;
639
+ if (compactHeader) {
640
+ return createElement(ChromePanel, {
641
+ backgroundColor: theme.switcherBackground,
642
+ marginBottom: 1,
643
+ }, createElement(Text, {
644
+ wrap: 'wrap',
645
+ }, createElement(Text, { color: theme.accentColor }, '●'), ' ', createElement(Text, { bold: true }, 'Murph')), props.bindingSummary
646
+ ? createElement(Text, {
647
+ color: theme.mutedColor,
648
+ wrap: 'wrap',
649
+ }, props.bindingSummary)
650
+ : null);
651
+ }
652
+ return createElement(Box, {
653
+ flexDirection: 'column',
654
+ marginBottom: 1,
655
+ width: '100%',
656
+ }, createElement(ChromePanel, {
657
+ backgroundColor: theme.switcherBackground,
658
+ marginBottom: 1,
659
+ }, createElement(Text, {
660
+ wrap: 'wrap',
661
+ }, createElement(Text, { color: theme.accentColor }, '●'), ' ', createElement(Text, { bold: true }, 'Murph'), ' ', createElement(Text, { color: theme.mutedColor }, 'interactive chat'))), createElement(ChromePanel, {
662
+ backgroundColor: theme.switcherBackground,
663
+ marginBottom: 0,
664
+ }, createElement(Text, {
665
+ color: theme.mutedColor,
666
+ wrap: 'wrap',
667
+ }, createElement(Text, { color: theme.accentColor }, '↳'), ` ${props.bindingSummary ?? 'local transcript-backed session'}`)));
668
+ });
669
+ const ChatEntryRow = React.memo(function ChatEntryRow(props) {
670
+ const createElement = React.createElement;
671
+ const theme = useAssistantInkTheme();
672
+ const { stdout } = useStdout();
673
+ const rowWidth = resolveAssistantChatViewportWidth(stdout?.columns);
674
+ if (props.entry.kind === 'assistant') {
675
+ return createElement(ChromePanel, {
676
+ marginBottom: 1,
677
+ width: rowWidth,
678
+ }, createElement(AssistantMessageText, { text: props.entry.text }));
679
+ }
680
+ if (props.entry.kind === 'error') {
681
+ return createElement(ChromePanel, {
682
+ backgroundColor: theme.switcherBackground,
683
+ marginBottom: 1,
684
+ width: rowWidth,
685
+ }, createElement(MessageRoleLabel, {
686
+ kind: 'error',
687
+ }), createElement(WrappedTextBlock, {
688
+ color: theme.errorColor,
689
+ }, props.entry.text));
690
+ }
691
+ if (props.entry.kind === 'trace') {
692
+ return createElement(Box, {
693
+ marginBottom: 1,
694
+ paddingLeft: 2,
695
+ width: rowWidth,
696
+ }, createElement(WrappedPlainTextBlock, {
697
+ columns: Math.max(1, rowWidth - 2),
698
+ dimColor: true,
699
+ text: `${props.entry.pending ? '· ' : ' '}${props.entry.text}`,
700
+ }));
701
+ }
702
+ if (props.entry.kind === 'thinking' || props.entry.kind === 'status') {
703
+ return createElement(Box, {
704
+ marginBottom: 1,
705
+ width: rowWidth,
706
+ }, createElement(Box, {
707
+ flexDirection: 'row',
708
+ width: rowWidth,
709
+ }, createElement(Text, { dimColor: true }, props.entry.kind === 'thinking' ? '· ' : '↻ '), createElement(Box, {
710
+ flexDirection: 'column',
711
+ flexGrow: 1,
712
+ flexShrink: 1,
713
+ }, createElement(WrappedTextBlock, {
714
+ dimColor: true,
715
+ }, props.entry.text))));
716
+ }
717
+ return createElement(ChromePanel, {
718
+ backgroundColor: theme.composerBackground,
719
+ marginBottom: 1,
720
+ paddingY: 1,
721
+ width: rowWidth,
722
+ }, createElement(Box, {
723
+ flexDirection: 'row',
724
+ width: '100%',
725
+ }, createElement(Text, {
726
+ color: theme.composerTextColor,
727
+ }, '› '), createElement(Box, {
728
+ flexDirection: 'column',
729
+ flexGrow: 1,
730
+ flexShrink: 1,
731
+ }, createElement(Text, {
732
+ color: theme.composerTextColor,
733
+ wrap: 'wrap',
734
+ }, props.entry.text))));
735
+ });
736
+ const AssistantMessageText = React.memo(function AssistantMessageText(props) {
737
+ return renderAssistantMessageText(props);
738
+ });
739
+ export function renderChatTranscriptFeed(input) {
740
+ const createElement = React.createElement;
741
+ const { liveEntries, staticEntries } = partitionChatTranscriptEntries({
742
+ busy: input.busy,
743
+ entries: input.entries,
744
+ });
745
+ const staticRows = [
746
+ {
747
+ kind: 'header',
748
+ bindingSummary: input.bindingSummary,
749
+ sessionId: input.sessionId,
750
+ },
751
+ ...staticEntries.map((entry) => ({
752
+ kind: 'entry',
753
+ entry,
754
+ })),
755
+ ];
756
+ return createElement(React.Fragment, {}, createElement(StaticTranscript, {
757
+ items: staticRows,
758
+ children: renderStaticTranscriptRow,
759
+ }), createElement(Box, {
760
+ flexDirection: 'column',
761
+ width: '100%',
762
+ }, ...liveEntries.map((entry, index) => createElement(ChatEntryRow, {
763
+ key: `live-entry:${staticEntries.length + index}`,
764
+ entry,
765
+ }))));
766
+ }
767
+ export function partitionChatTranscriptEntries(input) {
768
+ if (input.entries.length === 0) {
769
+ return {
770
+ liveEntries: [],
771
+ staticEntries: [],
772
+ };
773
+ }
774
+ if (!input.busy) {
775
+ return {
776
+ liveEntries: [],
777
+ staticEntries: [...input.entries],
778
+ };
779
+ }
780
+ let lastUserEntryIndex = -1;
781
+ for (let index = input.entries.length - 1; index >= 0; index -= 1) {
782
+ const entry = input.entries[index];
783
+ if (entry?.kind === 'user') {
784
+ lastUserEntryIndex = index;
785
+ break;
786
+ }
787
+ }
788
+ if (lastUserEntryIndex < 0) {
789
+ return {
790
+ liveEntries: [...input.entries],
791
+ staticEntries: [],
792
+ };
793
+ }
794
+ return {
795
+ liveEntries: input.entries.slice(lastUserEntryIndex + 1),
796
+ staticEntries: input.entries.slice(0, lastUserEntryIndex + 1),
797
+ };
798
+ }
799
+ export function shouldShowBusyStatus(input) {
800
+ if (!input.busy) {
801
+ return false;
802
+ }
803
+ let lastUserEntryIndex = -1;
804
+ for (let index = input.entries.length - 1; index >= 0; index -= 1) {
805
+ if (input.entries[index]?.kind === 'user') {
806
+ lastUserEntryIndex = index;
807
+ break;
808
+ }
809
+ }
810
+ if (lastUserEntryIndex < 0) {
811
+ return true;
812
+ }
813
+ for (let index = lastUserEntryIndex + 1; index < input.entries.length; index += 1) {
814
+ const entry = input.entries[index];
815
+ if (entry?.kind === 'assistant' && normalizeNullableString(entry.text)) {
816
+ return false;
817
+ }
818
+ if (entry?.kind === 'error' && normalizeNullableString(entry.text)) {
819
+ return false;
820
+ }
821
+ }
822
+ return true;
823
+ }
824
+ function renderStaticTranscriptRow(item, index) {
825
+ const createElement = React.createElement;
826
+ if (item.kind === 'header') {
827
+ return createElement(ChatHeader, {
828
+ key: `static-header:${item.sessionId}`,
829
+ bindingSummary: item.bindingSummary,
830
+ });
831
+ }
832
+ return createElement(ChatEntryRow, {
833
+ key: `static-entry:${index}`,
834
+ entry: item.entry,
835
+ });
836
+ }
837
+ const ChatTranscriptFeed = React.memo(function ChatTranscriptFeed(input) {
838
+ return renderChatTranscriptFeed(input);
839
+ });
840
+ const ChatStatus = React.memo(function ChatStatus(props) {
841
+ const createElement = React.createElement;
842
+ const theme = useAssistantInkTheme();
843
+ const { stdout } = useStdout();
844
+ const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
845
+ if (props.busy) {
846
+ const busyColor = props.status?.kind === 'error'
847
+ ? theme.errorColor
848
+ : props.status?.kind === 'success'
849
+ ? theme.successColor
850
+ : theme.infoColor;
851
+ const busyDetail = normalizeNullableString(props.status?.text);
852
+ const busyLabel = 'Working';
853
+ return createElement(ChromePanel, {
854
+ marginBottom: 1,
855
+ }, createElement(Box, {
856
+ flexDirection: 'row',
857
+ width: '100%',
858
+ }, createElement(BusySpinner, {
859
+ color: busyColor,
860
+ }), createElement(Text, {}, ' '), createElement(Box, {
861
+ flexDirection: 'column',
862
+ flexGrow: 1,
863
+ flexShrink: 1,
864
+ }, createElement(Text, {
865
+ color: busyColor,
866
+ }, busyLabel), busyDetail
867
+ ? createElement(WrappedPlainTextBlock, {
868
+ color: theme.mutedColor,
869
+ columns: wrapColumns,
870
+ text: busyDetail,
871
+ })
872
+ : null)));
873
+ }
874
+ if (!props.status) {
875
+ return null;
876
+ }
877
+ const statusColor = props.status.kind === 'error'
878
+ ? theme.errorColor
879
+ : props.status.kind === 'success'
880
+ ? theme.successColor
881
+ : theme.infoColor;
882
+ const statusIcon = props.status.kind === 'error'
883
+ ? '!'
884
+ : props.status.kind === 'success'
885
+ ? '✓'
886
+ : 'ℹ';
887
+ return createElement(ChromePanel, {
888
+ backgroundColor: theme.switcherBackground,
889
+ marginBottom: 1,
890
+ }, createElement(Box, {
891
+ flexDirection: 'row',
892
+ width: '100%',
893
+ }, createElement(Text, { color: statusColor }, `${statusIcon} `), createElement(Box, {
894
+ flexDirection: 'column',
895
+ flexGrow: 1,
896
+ flexShrink: 1,
897
+ }, createElement(WrappedPlainTextBlock, {
898
+ color: props.status.kind === 'info'
899
+ ? theme.composerTextColor
900
+ : statusColor,
901
+ columns: Math.max(1, wrapColumns - 2),
902
+ text: props.status.text,
903
+ }))));
904
+ });
905
+ const ChatComposer = React.memo(function ChatComposer(props) {
906
+ const createElement = React.createElement;
907
+ const theme = useAssistantInkTheme();
908
+ const { stdout } = useStdout();
909
+ const slashSuggestions = props.modelSwitcherActive
910
+ ? []
911
+ : getMatchingSlashCommands(props.value);
912
+ const showComposerGuidance = shouldShowChatComposerGuidance(props.entryCount);
913
+ const showStarterSuggestions = showComposerGuidance &&
914
+ !props.modelSwitcherActive &&
915
+ props.value.trim().length === 0;
916
+ const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
917
+ return createElement(React.Fragment, {}, createElement(ChromePanel, {
918
+ backgroundColor: theme.composerBackground,
919
+ marginBottom: slashSuggestions.length > 0 ? 0 : 1,
920
+ paddingY: 1,
921
+ }, createElement(Box, {
922
+ flexDirection: 'row',
923
+ width: '100%',
924
+ }, createElement(Text, { color: theme.composerTextColor }, '› '), createElement(ComposerInput, {
925
+ disabled: props.modelSwitcherActive,
926
+ value: props.value,
927
+ placeholder: 'Type a message',
928
+ onChange: props.onChange,
929
+ onEditLastQueuedPrompt: props.onEditLastQueuedPrompt,
930
+ onSubmit: props.onSubmit,
931
+ })), showComposerGuidance
932
+ ? createElement(Box, {
933
+ marginTop: 1,
934
+ }, createElement(Text, {
935
+ color: theme.mutedColor,
936
+ wrap: 'wrap',
937
+ }, CHAT_COMPOSER_HINT))
938
+ : null, showStarterSuggestions
939
+ ? createElement(Box, {
940
+ marginTop: 1,
941
+ width: '100%',
942
+ }, createElement(Text, { color: theme.mutedColor }, 'try:'), createElement(WrappedPlainTextBlock, {
943
+ color: theme.accentColor,
944
+ columns: Math.max(1, wrapColumns - 2),
945
+ text: ` ${CHAT_STARTER_SUGGESTIONS.join(' · ')}`,
946
+ }))
947
+ : null), createElement(SlashCommandSuggestions, {
948
+ commands: slashSuggestions,
949
+ }));
950
+ });
951
+ const QueuedFollowUpStatus = React.memo(function QueuedFollowUpStatus(props) {
952
+ const createElement = React.createElement;
953
+ const theme = useAssistantInkTheme();
954
+ const { stdout } = useStdout();
955
+ if (props.queuedPromptCount === 0 || !props.latestPrompt) {
956
+ return null;
957
+ }
958
+ const extraQueuedCount = props.queuedPromptCount - 1;
959
+ const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
960
+ return createElement(Box, {
961
+ flexDirection: 'column',
962
+ marginBottom: 1,
963
+ width: '100%',
964
+ }, createElement(Text, {
965
+ color: theme.composerTextColor,
966
+ wrap: 'wrap',
967
+ }, '• Queued follow-up messages'), createElement(WrappedPlainTextBlock, {
968
+ color: theme.composerTextColor,
969
+ columns: wrapColumns,
970
+ text: ` ↳ ${formatQueuedFollowUpPreview(props.latestPrompt)}`,
971
+ }), extraQueuedCount > 0
972
+ ? createElement(WrappedPlainTextBlock, {
973
+ color: theme.mutedColor,
974
+ columns: wrapColumns,
975
+ text: ` +${extraQueuedCount} more queued`,
976
+ })
977
+ : null, createElement(WrappedPlainTextBlock, {
978
+ color: theme.mutedColor,
979
+ columns: wrapColumns,
980
+ text: ` ${QUEUED_FOLLOW_UP_SHORTCUT_HINT}`,
981
+ }));
982
+ });
983
+ const ChatFooter = React.memo(function ChatFooter(props) {
984
+ const createElement = React.createElement;
985
+ return createElement(Box, {
986
+ flexDirection: 'column',
987
+ width: '100%',
988
+ }, createElement(Text, {
989
+ wrap: 'wrap',
990
+ }, ...props.badges.flatMap((badge, index) => [
991
+ index > 0 ? ' ' : '',
992
+ createElement(FooterBadge, {
993
+ badge,
994
+ key: `badge:${badge.key}`,
995
+ }),
996
+ ])));
997
+ });
998
+ function enqueuePendingComposerValue(pendingValues, nextValue) {
999
+ return pendingValues[pendingValues.length - 1] === nextValue
1000
+ ? [...pendingValues]
1001
+ : [...pendingValues, nextValue];
1002
+ }
1003
+ export function reconcileComposerControlledValue(input) {
1004
+ // Controlled updates that match a queued local value are only acknowledgements
1005
+ // from the parent state, so keep the newest local draft visible until the last
1006
+ // pending value is observed. Anything else is an external restore/reset and
1007
+ // should replace the live draft immediately.
1008
+ const nextControlledValue = input.nextControlledValue;
1009
+ const currentValue = input.currentValue;
1010
+ const clampedCursorOffset = clampComposerCursorOffset(input.cursorOffset, currentValue.length);
1011
+ if (nextControlledValue === input.previousControlledValue) {
1012
+ return {
1013
+ cursorOffset: clampedCursorOffset,
1014
+ nextValue: currentValue,
1015
+ pendingValues: [...input.pendingValues],
1016
+ };
1017
+ }
1018
+ const matchedPendingIndex = input.pendingValues.indexOf(nextControlledValue);
1019
+ if (matchedPendingIndex >= 0) {
1020
+ const remainingPendingValues = input.pendingValues.slice(matchedPendingIndex + 1);
1021
+ const nextValue = remainingPendingValues.length === 0 ? nextControlledValue : currentValue;
1022
+ return {
1023
+ cursorOffset: clampComposerCursorOffset(clampedCursorOffset, nextValue.length),
1024
+ nextValue,
1025
+ pendingValues: remainingPendingValues,
1026
+ };
1027
+ }
1028
+ return {
1029
+ cursorOffset: nextControlledValue.length,
1030
+ nextValue: nextControlledValue,
1031
+ pendingValues: [],
1032
+ };
1033
+ }
1034
+ function clampComposerCursorOffset(offset, valueLength) {
1035
+ return Math.max(0, Math.min(offset, valueLength));
1036
+ }
1037
+ function isComposerWordSeparator(character) {
1038
+ return COMPOSER_WORD_SEPARATORS.includes(character);
1039
+ }
1040
+ function isComposerWhitespace(character) {
1041
+ return /\s/u.test(character);
1042
+ }
1043
+ function moveComposerCursorLeft(state) {
1044
+ return {
1045
+ ...state,
1046
+ cursorOffset: clampComposerCursorOffset(state.cursorOffset - 1, state.value.length),
1047
+ };
1048
+ }
1049
+ function moveComposerCursorRight(state) {
1050
+ return {
1051
+ ...state,
1052
+ cursorOffset: clampComposerCursorOffset(state.cursorOffset + 1, state.value.length),
1053
+ };
1054
+ }
1055
+ function moveComposerCursorToStart(state) {
1056
+ return {
1057
+ ...state,
1058
+ cursorOffset: 0,
1059
+ };
1060
+ }
1061
+ function moveComposerCursorToEnd(state) {
1062
+ return {
1063
+ ...state,
1064
+ cursorOffset: state.value.length,
1065
+ };
1066
+ }
1067
+ function resolveComposerLineRanges(value) {
1068
+ const ranges = [];
1069
+ let lineStart = 0;
1070
+ for (let index = 0; index < value.length; index += 1) {
1071
+ if (value[index] !== '\n') {
1072
+ continue;
1073
+ }
1074
+ ranges.push({
1075
+ end: index,
1076
+ start: lineStart,
1077
+ });
1078
+ lineStart = index + 1;
1079
+ }
1080
+ ranges.push({
1081
+ end: value.length,
1082
+ start: lineStart,
1083
+ });
1084
+ return ranges;
1085
+ }
1086
+ function resolveComposerCursorLocation(value, cursorOffset) {
1087
+ const clampedCursorOffset = clampComposerCursorOffset(cursorOffset, value.length);
1088
+ let lineIndex = 0;
1089
+ let lineStart = 0;
1090
+ for (let index = 0; index < clampedCursorOffset; index += 1) {
1091
+ if (value[index] !== '\n') {
1092
+ continue;
1093
+ }
1094
+ lineIndex += 1;
1095
+ lineStart = index + 1;
1096
+ }
1097
+ return {
1098
+ column: clampedCursorOffset - lineStart,
1099
+ lineIndex,
1100
+ };
1101
+ }
1102
+ export function resolveComposerVerticalCursorMove(input) {
1103
+ const clampedCursorOffset = clampComposerCursorOffset(input.cursorOffset, input.value.length);
1104
+ const lineRanges = resolveComposerLineRanges(input.value);
1105
+ const currentLocation = resolveComposerCursorLocation(input.value, clampedCursorOffset);
1106
+ const targetLineIndex = input.direction === 'up'
1107
+ ? currentLocation.lineIndex - 1
1108
+ : currentLocation.lineIndex + 1;
1109
+ if (targetLineIndex < 0 || targetLineIndex >= lineRanges.length) {
1110
+ return {
1111
+ cursorOffset: clampedCursorOffset,
1112
+ preferredColumn: input.preferredColumn,
1113
+ };
1114
+ }
1115
+ const desiredColumn = input.preferredColumn ?? currentLocation.column;
1116
+ const targetLine = lineRanges[targetLineIndex];
1117
+ if (!targetLine) {
1118
+ return {
1119
+ cursorOffset: clampedCursorOffset,
1120
+ preferredColumn: input.preferredColumn,
1121
+ };
1122
+ }
1123
+ return {
1124
+ cursorOffset: targetLine.start + Math.min(desiredColumn, targetLine.end - targetLine.start),
1125
+ preferredColumn: desiredColumn,
1126
+ };
1127
+ }
1128
+ function findComposerPreviousWordStart(value, cursorOffset) {
1129
+ let index = clampComposerCursorOffset(cursorOffset, value.length);
1130
+ while (index > 0) {
1131
+ const previousCharacter = value.slice(index - 1, index);
1132
+ if (!isComposerWhitespace(previousCharacter)) {
1133
+ break;
1134
+ }
1135
+ index -= 1;
1136
+ }
1137
+ if (index === 0) {
1138
+ return 0;
1139
+ }
1140
+ const previousCharacter = value.slice(index - 1, index);
1141
+ const separator = isComposerWordSeparator(previousCharacter);
1142
+ while (index > 0) {
1143
+ const character = value.slice(index - 1, index);
1144
+ if (isComposerWhitespace(character) ||
1145
+ isComposerWordSeparator(character) !== separator) {
1146
+ break;
1147
+ }
1148
+ index -= 1;
1149
+ }
1150
+ return index;
1151
+ }
1152
+ function findComposerNextWordEnd(value, cursorOffset) {
1153
+ let index = clampComposerCursorOffset(cursorOffset, value.length);
1154
+ while (index < value.length) {
1155
+ const character = value.slice(index, index + 1);
1156
+ if (!isComposerWhitespace(character)) {
1157
+ break;
1158
+ }
1159
+ index += 1;
1160
+ }
1161
+ if (index >= value.length) {
1162
+ return value.length;
1163
+ }
1164
+ const separator = isComposerWordSeparator(value.slice(index, index + 1));
1165
+ while (index < value.length) {
1166
+ const character = value.slice(index, index + 1);
1167
+ if (isComposerWhitespace(character) ||
1168
+ isComposerWordSeparator(character) !== separator) {
1169
+ break;
1170
+ }
1171
+ index += 1;
1172
+ }
1173
+ return index;
1174
+ }
1175
+ function moveComposerCursorToPreviousWord(state) {
1176
+ return {
1177
+ ...state,
1178
+ cursorOffset: findComposerPreviousWordStart(state.value, state.cursorOffset),
1179
+ };
1180
+ }
1181
+ function moveComposerCursorToNextWord(state) {
1182
+ return {
1183
+ ...state,
1184
+ cursorOffset: findComposerNextWordEnd(state.value, state.cursorOffset),
1185
+ };
1186
+ }
1187
+ function replaceComposerRange(state, range, replacement) {
1188
+ const nextValue = state.value.slice(0, range.start) + replacement + state.value.slice(range.end);
1189
+ return {
1190
+ ...state,
1191
+ cursorOffset: range.start + replacement.length,
1192
+ value: nextValue,
1193
+ };
1194
+ }
1195
+ function killComposerRange(state, range) {
1196
+ if (range.end <= range.start) {
1197
+ return state;
1198
+ }
1199
+ return {
1200
+ ...replaceComposerRange(state, range, ''),
1201
+ killBuffer: state.value.slice(range.start, range.end),
1202
+ };
1203
+ }
1204
+ function deleteComposerBackward(state) {
1205
+ if (state.cursorOffset <= 0) {
1206
+ return state;
1207
+ }
1208
+ return replaceComposerRange(state, {
1209
+ end: state.cursorOffset,
1210
+ start: state.cursorOffset - 1,
1211
+ }, '');
1212
+ }
1213
+ function deleteComposerForward(state) {
1214
+ if (state.cursorOffset >= state.value.length) {
1215
+ return state;
1216
+ }
1217
+ return replaceComposerRange(state, {
1218
+ end: state.cursorOffset + 1,
1219
+ start: state.cursorOffset,
1220
+ }, '');
1221
+ }
1222
+ function deleteComposerBackwardWord(state) {
1223
+ return killComposerRange(state, {
1224
+ end: state.cursorOffset,
1225
+ start: findComposerPreviousWordStart(state.value, state.cursorOffset),
1226
+ });
1227
+ }
1228
+ function deleteComposerForwardWord(state) {
1229
+ return killComposerRange(state, {
1230
+ end: findComposerNextWordEnd(state.value, state.cursorOffset),
1231
+ start: state.cursorOffset,
1232
+ });
1233
+ }
1234
+ function killComposerToStart(state) {
1235
+ return killComposerRange(state, {
1236
+ end: state.cursorOffset,
1237
+ start: 0,
1238
+ });
1239
+ }
1240
+ function killComposerToEnd(state) {
1241
+ return killComposerRange(state, {
1242
+ end: state.value.length,
1243
+ start: state.cursorOffset,
1244
+ });
1245
+ }
1246
+ function yankComposerKillBuffer(state) {
1247
+ if (state.killBuffer.length === 0) {
1248
+ return state;
1249
+ }
1250
+ return replaceComposerRange(state, {
1251
+ end: state.cursorOffset,
1252
+ start: state.cursorOffset,
1253
+ }, state.killBuffer);
1254
+ }
1255
+ function finalizeComposerEditingResult(next) {
1256
+ return {
1257
+ ...next,
1258
+ handled: true,
1259
+ };
1260
+ }
1261
+ export function applyComposerEditingInput(state, input, key) {
1262
+ const currentState = {
1263
+ ...state,
1264
+ cursorOffset: clampComposerCursorOffset(state.cursorOffset, state.value.length),
1265
+ };
1266
+ if (key.home || (key.super && key.leftArrow)) {
1267
+ return finalizeComposerEditingResult(moveComposerCursorToStart(currentState));
1268
+ }
1269
+ if (key.end || (key.super && key.rightArrow)) {
1270
+ return finalizeComposerEditingResult(moveComposerCursorToEnd(currentState));
1271
+ }
1272
+ if (key.leftArrow) {
1273
+ return finalizeComposerEditingResult(key.meta || key.ctrl
1274
+ ? moveComposerCursorToPreviousWord(currentState)
1275
+ : moveComposerCursorLeft(currentState));
1276
+ }
1277
+ if (key.rightArrow) {
1278
+ return finalizeComposerEditingResult(key.meta || key.ctrl
1279
+ ? moveComposerCursorToNextWord(currentState)
1280
+ : moveComposerCursorRight(currentState));
1281
+ }
1282
+ if (key.backspace) {
1283
+ return finalizeComposerEditingResult(key.super
1284
+ ? killComposerToStart(currentState)
1285
+ : key.meta
1286
+ ? deleteComposerBackwardWord(currentState)
1287
+ : deleteComposerBackward(currentState));
1288
+ }
1289
+ if (key.delete) {
1290
+ return finalizeComposerEditingResult(key.super
1291
+ ? killComposerToEnd(currentState)
1292
+ : key.meta
1293
+ ? deleteComposerForwardWord(currentState)
1294
+ : deleteComposerForward(currentState));
1295
+ }
1296
+ if (key.ctrl) {
1297
+ switch (input) {
1298
+ case 'a':
1299
+ return finalizeComposerEditingResult(moveComposerCursorToStart(currentState));
1300
+ case 'b':
1301
+ return finalizeComposerEditingResult(moveComposerCursorLeft(currentState));
1302
+ case 'd':
1303
+ return finalizeComposerEditingResult(deleteComposerForward(currentState));
1304
+ case 'e':
1305
+ return finalizeComposerEditingResult(moveComposerCursorToEnd(currentState));
1306
+ case 'f':
1307
+ return finalizeComposerEditingResult(moveComposerCursorRight(currentState));
1308
+ case 'h':
1309
+ return finalizeComposerEditingResult(deleteComposerBackward(currentState));
1310
+ case 'k':
1311
+ return finalizeComposerEditingResult(killComposerToEnd(currentState));
1312
+ case 'u':
1313
+ return finalizeComposerEditingResult(killComposerToStart(currentState));
1314
+ case 'w':
1315
+ return finalizeComposerEditingResult(deleteComposerBackwardWord(currentState));
1316
+ case 'y':
1317
+ return finalizeComposerEditingResult(yankComposerKillBuffer(currentState));
1318
+ default:
1319
+ break;
1320
+ }
1321
+ }
1322
+ if (key.meta) {
1323
+ switch (input) {
1324
+ case 'b':
1325
+ return finalizeComposerEditingResult(moveComposerCursorToPreviousWord(currentState));
1326
+ case 'd':
1327
+ return finalizeComposerEditingResult(deleteComposerForwardWord(currentState));
1328
+ case 'f':
1329
+ return finalizeComposerEditingResult(moveComposerCursorToNextWord(currentState));
1330
+ default:
1331
+ break;
1332
+ }
1333
+ }
1334
+ if (input.length === 0) {
1335
+ return {
1336
+ ...currentState,
1337
+ handled: false,
1338
+ };
1339
+ }
1340
+ const insertionText = normalizeComposerInsertedText(input);
1341
+ if (insertionText.length === 0) {
1342
+ return {
1343
+ ...currentState,
1344
+ handled: false,
1345
+ };
1346
+ }
1347
+ return finalizeComposerEditingResult(replaceComposerRange(currentState, {
1348
+ end: currentState.cursorOffset,
1349
+ start: currentState.cursorOffset,
1350
+ }, insertionText));
1351
+ }
1352
+ export function normalizeComposerInsertedText(input) {
1353
+ return input.replace(/\r\n?/gu, '\n');
1354
+ }
1355
+ function resolveComposerCursorDisplay(input) {
1356
+ const cursorOffset = clampComposerCursorOffset(input.cursorOffset, input.value.length);
1357
+ const beforeCursor = input.value.slice(0, cursorOffset);
1358
+ const rawCursorCharacter = input.value.slice(cursorOffset, cursorOffset + 1);
1359
+ const afterCursor = cursorOffset < input.value.length
1360
+ ? input.value.slice(cursorOffset + 1)
1361
+ : '';
1362
+ if (rawCursorCharacter === '\n') {
1363
+ return {
1364
+ afterCursor: `\n${input.value.slice(cursorOffset + 1)}`,
1365
+ beforeCursor,
1366
+ cursorCharacter: ' ',
1367
+ };
1368
+ }
1369
+ if (rawCursorCharacter.length === 0) {
1370
+ return {
1371
+ afterCursor: '',
1372
+ beforeCursor,
1373
+ cursorCharacter: ' ',
1374
+ };
1375
+ }
1376
+ return {
1377
+ afterCursor,
1378
+ beforeCursor,
1379
+ cursorCharacter: rawCursorCharacter,
1380
+ };
1381
+ }
1382
+ export function renderComposerValue(input) {
1383
+ const createElement = React.createElement;
1384
+ if (input.value.length === 0) {
1385
+ if (input.disabled) {
1386
+ return createElement(Text, {
1387
+ color: input.theme.composerPlaceholderColor,
1388
+ wrap: 'wrap',
1389
+ }, input.placeholder);
1390
+ }
1391
+ const cursorCharacter = input.placeholder.slice(0, 1) || ' ';
1392
+ const remainder = input.placeholder.slice(1);
1393
+ return createElement(Text, {
1394
+ color: input.theme.composerPlaceholderColor,
1395
+ wrap: 'wrap',
1396
+ }, createElement(Text, {
1397
+ backgroundColor: input.theme.composerCursorBackground,
1398
+ color: input.theme.composerCursorTextColor,
1399
+ }, cursorCharacter), remainder);
1400
+ }
1401
+ const cursorDisplay = resolveComposerCursorDisplay({
1402
+ cursorOffset: input.cursorOffset,
1403
+ value: input.value,
1404
+ });
1405
+ if (input.disabled) {
1406
+ return createElement(Text, {
1407
+ color: input.theme.composerTextColor,
1408
+ wrap: 'wrap',
1409
+ }, input.value);
1410
+ }
1411
+ return createElement(Text, {
1412
+ color: input.theme.composerTextColor,
1413
+ wrap: 'wrap',
1414
+ }, cursorDisplay.beforeCursor, createElement(Text, {
1415
+ backgroundColor: input.theme.composerCursorBackground,
1416
+ color: input.theme.composerCursorTextColor,
1417
+ }, cursorDisplay.cursorCharacter), cursorDisplay.afterCursor);
1418
+ }
1419
+ function ModelSwitcher(props) {
1420
+ const createElement = React.createElement;
1421
+ const theme = useAssistantInkTheme();
1422
+ const onCancelRef = React.useRef(props.onCancel);
1423
+ const onConfirmRef = React.useRef(props.onConfirm);
1424
+ const onMoveRef = React.useRef(props.onMove);
1425
+ onCancelRef.current = props.onCancel;
1426
+ onConfirmRef.current = props.onConfirm;
1427
+ onMoveRef.current = props.onMove;
1428
+ const handleModelSwitcherInput = React.useCallback((input, key) => {
1429
+ const normalizedKey = normalizeAssistantInkArrowKey(input, key);
1430
+ if (normalizedKey.escape) {
1431
+ onCancelRef.current();
1432
+ return;
1433
+ }
1434
+ if (normalizedKey.upArrow || input === 'k') {
1435
+ onMoveRef.current(-1);
1436
+ return;
1437
+ }
1438
+ if (normalizedKey.downArrow || input === 'j') {
1439
+ onMoveRef.current(1);
1440
+ return;
1441
+ }
1442
+ if (normalizedKey.return) {
1443
+ onConfirmRef.current();
1444
+ }
1445
+ }, []);
1446
+ useInput(handleModelSwitcherInput);
1447
+ const selectedModelLabel = props.modelOptions[props.modelIndex]?.value ??
1448
+ props.currentModel ??
1449
+ 'the current model';
1450
+ const canChooseReasoning = props.reasoningOptions.length > 0;
1451
+ const title = props.mode === 'model'
1452
+ ? 'Choose a model'
1453
+ : `Choose reasoning for ${selectedModelLabel}`;
1454
+ const subtitle = props.mode === 'model'
1455
+ ? canChooseReasoning
1456
+ ? 'Step 1 of 2. Enter continues to reasoning depth.'
1457
+ : 'Enter confirms the active model.'
1458
+ : 'Step 2 of 2. Enter confirms the active reasoning depth.';
1459
+ const helpText = props.mode === 'model'
1460
+ ? canChooseReasoning
1461
+ ? '↑/↓ move · Enter next · Esc close'
1462
+ : '↑/↓ move · Enter confirm · Esc close'
1463
+ : '↑/↓ move · Enter confirm · Esc back';
1464
+ const options = props.mode === 'model'
1465
+ ? props.modelOptions.map((option, index) => renderSwitcherRow({
1466
+ current: normalizeNullableString(option.value) ===
1467
+ normalizeNullableString(props.currentModel),
1468
+ description: option.description,
1469
+ index,
1470
+ label: option.value,
1471
+ selected: index === props.modelIndex,
1472
+ theme,
1473
+ }))
1474
+ : props.reasoningOptions.map((option, index) => renderSwitcherRow({
1475
+ current: isCurrentReasoningOption(option.value, props.currentReasoningEffort),
1476
+ description: option.description,
1477
+ index,
1478
+ label: option.value === 'medium'
1479
+ ? `${option.label} (default)`
1480
+ : option.label,
1481
+ selected: index === props.reasoningIndex,
1482
+ theme,
1483
+ }));
1484
+ return createElement(ChromePanel, {
1485
+ backgroundColor: theme.switcherBackground,
1486
+ marginBottom: 1,
1487
+ }, createElement(Box, {
1488
+ flexDirection: 'column',
1489
+ }, createElement(Text, {
1490
+ bold: true,
1491
+ color: theme.switcherTextColor,
1492
+ }, title), createElement(Text, {
1493
+ color: theme.switcherMutedColor,
1494
+ }, subtitle), createElement(Box, {
1495
+ height: 1,
1496
+ }), ...options, createElement(Box, {
1497
+ height: 1,
1498
+ }), createElement(Text, {
1499
+ color: theme.switcherMutedColor,
1500
+ }, helpText)));
1501
+ }
1502
+ function SlashCommandSuggestions(input) {
1503
+ const theme = useAssistantInkTheme();
1504
+ const { stdout } = useStdout();
1505
+ if (input.commands.length === 0) {
1506
+ return null;
1507
+ }
1508
+ const createElement = React.createElement;
1509
+ const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
1510
+ return createElement(ChromePanel, {
1511
+ backgroundColor: theme.switcherBackground,
1512
+ marginBottom: 1,
1513
+ }, createElement(Text, {
1514
+ bold: true,
1515
+ color: theme.mutedColor,
1516
+ }, 'commands'), ...input.commands.map((command) => createElement(Box, {
1517
+ flexDirection: 'row',
1518
+ key: command.command,
1519
+ width: '100%',
1520
+ }, createElement(Text, { color: theme.accentColor }, command.command), createElement(Box, {
1521
+ flexDirection: 'column',
1522
+ flexGrow: 1,
1523
+ flexShrink: 1,
1524
+ }, createElement(WrappedPlainTextBlock, {
1525
+ color: theme.mutedColor,
1526
+ columns: Math.max(1, wrapColumns - command.command.length - 2),
1527
+ text: ` ${command.description}`,
1528
+ })))));
1529
+ }
1530
+ function renderSwitcherRow(input) {
1531
+ const createElement = React.createElement;
1532
+ const textColor = input.selected
1533
+ ? input.theme.switcherSelectionTextColor
1534
+ : input.theme.switcherTextColor;
1535
+ const descriptionColor = input.selected
1536
+ ? input.theme.switcherSelectionTextColor
1537
+ : input.theme.switcherMutedColor;
1538
+ return createElement(Box, {
1539
+ backgroundColor: input.selected
1540
+ ? input.theme.switcherSelectionBackground
1541
+ : undefined,
1542
+ key: `${input.label}:${input.index}`,
1543
+ flexDirection: 'column',
1544
+ marginBottom: 1,
1545
+ paddingX: 1,
1546
+ width: '100%',
1547
+ }, createElement(Text, {
1548
+ color: textColor,
1549
+ }, createElement(Text, { color: textColor }, input.selected ? '●' : '○'), ` ${input.index + 1}. ${input.label}`, input.current
1550
+ ? createElement(Text, {
1551
+ color: input.selected ? textColor : input.theme.accentColor,
1552
+ }, ' · current')
1553
+ : null), createElement(Text, {
1554
+ color: descriptionColor,
1555
+ wrap: 'wrap',
1556
+ }, input.description));
1557
+ }
1558
+ function namespaceTurnTraceUpdates(updates, turnTracePrefix) {
1559
+ return updates.map((update) => ({
1560
+ ...update,
1561
+ streamKey: update.streamKey
1562
+ ? `${turnTracePrefix}:${update.streamKey}`
1563
+ : update.streamKey,
1564
+ }));
1565
+ }
1566
+ function namespaceProviderProgressEvent(event, turnTracePrefix) {
1567
+ return {
1568
+ ...event,
1569
+ id: event.id ? `${turnTracePrefix}:${event.id}` : `${turnTracePrefix}:trace`,
1570
+ };
1571
+ }
1572
+ function isCurrentReasoningOption(option, currentReasoningEffort) {
1573
+ const normalizedCurrent = normalizeNullableString(currentReasoningEffort) ?? 'medium';
1574
+ return normalizeNullableString(option) === normalizedCurrent;
1575
+ }
1576
+ function wrapPickerIndex(index, count) {
1577
+ if (count <= 0) {
1578
+ return 0;
1579
+ }
1580
+ return ((index % count) + count) % count;
1581
+ }
1582
+ function assistantModelDiscoveryResultsEqual(left, right) {
1583
+ if (left === right) {
1584
+ return true;
1585
+ }
1586
+ if (!left || !right) {
1587
+ return false;
1588
+ }
1589
+ return (left.status === right.status &&
1590
+ (normalizeNullableString(left.message) ?? null) ===
1591
+ (normalizeNullableString(right.message) ?? null) &&
1592
+ left.models.length === right.models.length &&
1593
+ left.models.every((model, index) => model.id === right.models[index]?.id));
1594
+ }
1595
+ const EMPTY_ASSISTANT_PROMPT_QUEUE_STATE = {
1596
+ prompts: [],
1597
+ };
1598
+ const IDLE_ASSISTANT_TURN_STATE = {
1599
+ pauseRequested: false,
1600
+ phase: 'idle',
1601
+ };
1602
+ export function reduceAssistantPromptQueueState(state, action) {
1603
+ switch (action.kind) {
1604
+ case 'clear':
1605
+ return EMPTY_ASSISTANT_PROMPT_QUEUE_STATE;
1606
+ case 'dequeue':
1607
+ return state.prompts.length > 0
1608
+ ? {
1609
+ prompts: state.prompts.slice(1),
1610
+ }
1611
+ : state;
1612
+ case 'enqueue':
1613
+ return {
1614
+ prompts: [...state.prompts, action.prompt],
1615
+ };
1616
+ case 'pop-last':
1617
+ return state.prompts.length > 0
1618
+ ? {
1619
+ prompts: state.prompts.slice(0, -1),
1620
+ }
1621
+ : state;
1622
+ default:
1623
+ return state;
1624
+ }
1625
+ }
1626
+ export function reduceAssistantTurnState(state, action) {
1627
+ switch (action.kind) {
1628
+ case 'finish':
1629
+ return IDLE_ASSISTANT_TURN_STATE;
1630
+ case 'request-pause':
1631
+ return state.phase === 'running'
1632
+ ? {
1633
+ ...state,
1634
+ pauseRequested: true,
1635
+ }
1636
+ : state;
1637
+ case 'start':
1638
+ return {
1639
+ pauseRequested: false,
1640
+ phase: 'running',
1641
+ };
1642
+ default:
1643
+ return state;
1644
+ }
1645
+ }
1646
+ function createAssistantTurnTracePrefix() {
1647
+ return `turn:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
1648
+ }
1649
+ export function resolveAssistantQueuedPromptDisposition(input) {
1650
+ if (input.turnOutcome === 'failed' ||
1651
+ input.turnOutcome === 'interrupted' ||
1652
+ (input.pauseRequested && input.turnOutcome === 'completed')) {
1653
+ return {
1654
+ kind: 'restore-composer',
1655
+ restoredQueuedPromptCount: input.queuedPrompts.length,
1656
+ };
1657
+ }
1658
+ if (input.turnOutcome === 'completed' && input.queuedPrompts.length > 0) {
1659
+ return {
1660
+ kind: 'replay-next',
1661
+ nextQueuedPrompt: input.queuedPrompts[0] ?? '',
1662
+ remainingQueuedPrompts: input.queuedPrompts.slice(1),
1663
+ };
1664
+ }
1665
+ return {
1666
+ kind: 'idle',
1667
+ };
1668
+ }
1669
+ function normalizeAssistantTurnSelection(input) {
1670
+ return {
1671
+ activeModel: normalizeNullableString(input.activeModel),
1672
+ activeReasoningEffort: normalizeNullableString(input.activeReasoningEffort),
1673
+ };
1674
+ }
1675
+ function resolveAssistantSessionTurnSelection(session) {
1676
+ return normalizeAssistantTurnSelection({
1677
+ activeModel: session.providerOptions.model,
1678
+ activeReasoningEffort: session.providerOptions.reasoningEffort,
1679
+ });
1680
+ }
1681
+ export function resolveAssistantSelectionAfterSessionSync(input) {
1682
+ const currentSelection = normalizeAssistantTurnSelection(input.currentSelection);
1683
+ const previousSessionSelection = resolveAssistantSessionTurnSelection(input.previousSession);
1684
+ const nextSessionSelection = resolveAssistantSessionTurnSelection(input.nextSession);
1685
+ const effectiveSelectionChanged = input.previousSession.provider !== input.nextSession.provider ||
1686
+ previousSessionSelection.activeModel !== nextSessionSelection.activeModel ||
1687
+ previousSessionSelection.activeReasoningEffort !==
1688
+ nextSessionSelection.activeReasoningEffort;
1689
+ return effectiveSelectionChanged ? nextSessionSelection : currentSelection;
1690
+ }
1691
+ export async function runAssistantPromptTurn(input) {
1692
+ let streamedAssistantEntryKey = null;
1693
+ const handleTraceEvent = (event) => {
1694
+ const namespacedUpdates = namespaceTurnTraceUpdates(event.updates, input.turnTracePrefix);
1695
+ if (namespacedUpdates.length === 0) {
1696
+ return;
1697
+ }
1698
+ for (const update of namespacedUpdates) {
1699
+ if (update.kind === 'assistant' && update.streamKey) {
1700
+ streamedAssistantEntryKey = streamedAssistantEntryKey ?? update.streamKey;
1701
+ }
1702
+ }
1703
+ input.setEntries((previous) => applyInkChatTraceUpdates(previous, namespacedUpdates));
1704
+ const latestStatusUpdate = [...namespacedUpdates]
1705
+ .reverse()
1706
+ .find((update) => update.kind === 'error' || update.kind === 'status');
1707
+ if (latestStatusUpdate) {
1708
+ input.setStatus({
1709
+ kind: latestStatusUpdate.kind === 'error' ? 'error' : 'info',
1710
+ text: latestStatusUpdate.text,
1711
+ });
1712
+ }
1713
+ };
1714
+ try {
1715
+ const result = await sendAssistantMessage({
1716
+ ...input.input,
1717
+ abortSignal: input.input.abortSignal,
1718
+ conversation: {
1719
+ ...(input.input.conversation ?? {}),
1720
+ sessionId: input.session.sessionId,
1721
+ },
1722
+ model: input.activeModel,
1723
+ onProviderEvent: (event) => {
1724
+ input.setEntries((previous) => applyProviderProgressEventToEntries({
1725
+ entries: previous,
1726
+ event: namespaceProviderProgressEvent(event, input.turnTracePrefix),
1727
+ }));
1728
+ },
1729
+ onTraceEvent: handleTraceEvent,
1730
+ prompt: input.prompt,
1731
+ reasoningEffort: input.activeReasoningEffort,
1732
+ showThinkingTraces: true,
1733
+ });
1734
+ return {
1735
+ delivery: result.delivery,
1736
+ deliveryError: result.deliveryError,
1737
+ kind: 'completed',
1738
+ response: result.response,
1739
+ session: result.session,
1740
+ streamedAssistantEntryKey,
1741
+ };
1742
+ }
1743
+ catch (error) {
1744
+ const recoveredSession = extractRecoveredAssistantSession(error);
1745
+ if (isAssistantProviderInterruptedError(error)) {
1746
+ return {
1747
+ kind: 'interrupted',
1748
+ recoveredSession,
1749
+ };
1750
+ }
1751
+ return {
1752
+ error,
1753
+ kind: 'failed',
1754
+ recoveredSession,
1755
+ };
1756
+ }
1757
+ }
1758
+ function useAssistantChatController(input) {
1759
+ const { exit } = useApp();
1760
+ const [session, setSession] = React.useState(input.resolvedSession);
1761
+ const [entries, setEntries] = React.useState(seedChatEntries(input.transcriptEntries));
1762
+ const entriesRef = React.useRef(entries);
1763
+ const [status, setStatus] = React.useState(null);
1764
+ const [composerValue, setComposerValue] = React.useState('');
1765
+ const initialActiveModel = normalizeNullableString(input.input.model) ??
1766
+ normalizeNullableString(input.selectedProviderDefaults?.model) ??
1767
+ normalizeNullableString(input.resolvedSession.providerOptions.model) ??
1768
+ normalizeNullableString(input.codexDisplay.model);
1769
+ const initialActiveReasoningEffort = normalizeNullableString(input.input.reasoningEffort) ??
1770
+ normalizeNullableString(input.selectedProviderDefaults?.reasoningEffort) ??
1771
+ normalizeNullableString(input.resolvedSession.providerOptions.reasoningEffort) ??
1772
+ normalizeNullableString(input.codexDisplay.reasoningEffort);
1773
+ const [activeModel, setActiveModel] = React.useState(initialActiveModel);
1774
+ const [activeReasoningEffort, setActiveReasoningEffort] = React.useState(initialActiveReasoningEffort);
1775
+ const [modelDiscovery, setModelDiscovery] = React.useState(null);
1776
+ const [modelSwitcherState, setModelSwitcherState] = React.useState(null);
1777
+ const [promptQueueState, setPromptQueueState] = React.useState(EMPTY_ASSISTANT_PROMPT_QUEUE_STATE);
1778
+ const [turnState, setTurnState] = React.useState(IDLE_ASSISTANT_TURN_STATE);
1779
+ const latestSessionRef = React.useRef(input.resolvedSession);
1780
+ const latestTurnsRef = React.useRef(0);
1781
+ const initialPromptRef = React.useRef(normalizeNullableString(input.input.initialPrompt));
1782
+ const bootstrappedRef = React.useRef(false);
1783
+ const promptQueueStateRef = React.useRef(EMPTY_ASSISTANT_PROMPT_QUEUE_STATE);
1784
+ const turnStateRef = React.useRef(IDLE_ASSISTANT_TURN_STATE);
1785
+ const activeTurnAbortControllerRef = React.useRef(null);
1786
+ const activeSelectionRef = React.useRef({
1787
+ activeModel: initialActiveModel,
1788
+ activeReasoningEffort: initialActiveReasoningEffort,
1789
+ });
1790
+ const modelCatalog = resolveAssistantModelCatalog({
1791
+ provider: session.provider,
1792
+ baseUrl: session.providerOptions.baseUrl,
1793
+ currentModel: activeModel,
1794
+ currentReasoningEffort: activeReasoningEffort,
1795
+ discovery: modelDiscovery,
1796
+ headers: session.providerOptions.headers ?? null,
1797
+ apiKeyEnv: session.providerOptions.apiKeyEnv,
1798
+ oss: session.providerOptions.oss,
1799
+ providerName: session.providerOptions.providerName,
1800
+ });
1801
+ const updatePromptQueue = React.useCallback((action) => {
1802
+ const nextState = reduceAssistantPromptQueueState(promptQueueStateRef.current, action);
1803
+ promptQueueStateRef.current = nextState;
1804
+ setPromptQueueState(nextState);
1805
+ return nextState;
1806
+ }, []);
1807
+ const updateTurnState = React.useCallback((action) => {
1808
+ const nextState = reduceAssistantTurnState(turnStateRef.current, action);
1809
+ turnStateRef.current = nextState;
1810
+ setTurnState(nextState);
1811
+ return nextState;
1812
+ }, []);
1813
+ React.useEffect(() => {
1814
+ latestSessionRef.current = session;
1815
+ }, [session]);
1816
+ React.useEffect(() => {
1817
+ entriesRef.current = entries;
1818
+ }, [entries]);
1819
+ const setActiveSelection = React.useCallback((nextSelection) => {
1820
+ const normalizedSelection = normalizeAssistantTurnSelection(nextSelection);
1821
+ activeSelectionRef.current = normalizedSelection;
1822
+ setActiveModel(normalizedSelection.activeModel);
1823
+ setActiveReasoningEffort(normalizedSelection.activeReasoningEffort);
1824
+ }, []);
1825
+ const commitSession = React.useCallback((nextSession) => {
1826
+ const previousSession = latestSessionRef.current;
1827
+ latestSessionRef.current = nextSession;
1828
+ setSession(nextSession);
1829
+ const nextSelection = resolveAssistantSelectionAfterSessionSync({
1830
+ currentSelection: activeSelectionRef.current,
1831
+ previousSession,
1832
+ nextSession,
1833
+ });
1834
+ if (nextSelection.activeModel !== activeSelectionRef.current.activeModel ||
1835
+ nextSelection.activeReasoningEffort !==
1836
+ activeSelectionRef.current.activeReasoningEffort) {
1837
+ setActiveSelection(nextSelection);
1838
+ }
1839
+ }, [setActiveSelection]);
1840
+ React.useEffect(() => {
1841
+ let cancelled = false;
1842
+ const baseUrl = normalizeNullableString(session.providerOptions.baseUrl);
1843
+ if (!modelCatalog.capabilities.supportsModelDiscovery || !baseUrl) {
1844
+ setModelDiscovery((existing) => (existing === null ? existing : null));
1845
+ return () => {
1846
+ cancelled = true;
1847
+ };
1848
+ }
1849
+ void (async () => {
1850
+ const nextDiscovery = await discoverAssistantProviderModels({
1851
+ provider: session.provider,
1852
+ baseUrl,
1853
+ apiKeyEnv: session.providerOptions.apiKeyEnv,
1854
+ headers: session.providerOptions.headers ?? null,
1855
+ providerName: session.providerOptions.providerName,
1856
+ });
1857
+ if (cancelled) {
1858
+ return;
1859
+ }
1860
+ setModelDiscovery((existing) => assistantModelDiscoveryResultsEqual(existing, nextDiscovery)
1861
+ ? existing
1862
+ : nextDiscovery);
1863
+ })();
1864
+ return () => {
1865
+ cancelled = true;
1866
+ };
1867
+ }, [
1868
+ modelCatalog.capabilities.supportsModelDiscovery,
1869
+ session.provider,
1870
+ session.providerOptions.apiKeyEnv,
1871
+ session.providerOptions.baseUrl,
1872
+ session.providerOptions.headers,
1873
+ session.providerOptions.providerName,
1874
+ ]);
1875
+ const queuePrompt = (prompt) => {
1876
+ updatePromptQueue({
1877
+ kind: 'enqueue',
1878
+ prompt,
1879
+ });
1880
+ };
1881
+ const applyQueuedPromptDisposition = (disposition, queuedPrompts) => {
1882
+ if (disposition.kind === 'restore-composer') {
1883
+ updatePromptQueue({
1884
+ kind: 'clear',
1885
+ });
1886
+ if (queuedPrompts.length > 0) {
1887
+ setComposerValue((previous) => mergeComposerDraftWithQueuedPrompts(previous, queuedPrompts));
1888
+ }
1889
+ return null;
1890
+ }
1891
+ if (disposition.kind === 'replay-next') {
1892
+ promptQueueStateRef.current = {
1893
+ prompts: disposition.remainingQueuedPrompts,
1894
+ };
1895
+ setPromptQueueState(promptQueueStateRef.current);
1896
+ return disposition.nextQueuedPrompt;
1897
+ }
1898
+ return null;
1899
+ };
1900
+ const editLastQueuedPrompt = () => {
1901
+ const lastQueuedPrompt = promptQueueStateRef.current.prompts.at(-1);
1902
+ if (!lastQueuedPrompt) {
1903
+ return;
1904
+ }
1905
+ updatePromptQueue({
1906
+ kind: 'pop-last',
1907
+ });
1908
+ setComposerValue((previous) => mergeComposerDraftWithQueuedPrompts(previous, [lastQueuedPrompt]));
1909
+ };
1910
+ const startPromptTurn = (prompt) => {
1911
+ setEntries((previous) => [
1912
+ ...previous,
1913
+ {
1914
+ kind: 'user',
1915
+ text: prompt,
1916
+ },
1917
+ ]);
1918
+ setStatus(null);
1919
+ updateTurnState({
1920
+ kind: 'start',
1921
+ });
1922
+ const abortController = new AbortController();
1923
+ const turnTracePrefix = createAssistantTurnTracePrefix();
1924
+ activeTurnAbortControllerRef.current = abortController;
1925
+ void (async () => {
1926
+ const activeSelection = activeSelectionRef.current;
1927
+ const outcome = await runAssistantPromptTurn({
1928
+ activeModel: activeSelection.activeModel,
1929
+ activeReasoningEffort: activeSelection.activeReasoningEffort,
1930
+ input: {
1931
+ ...input.input,
1932
+ abortSignal: abortController.signal,
1933
+ },
1934
+ prompt,
1935
+ session: latestSessionRef.current,
1936
+ setEntries,
1937
+ setStatus,
1938
+ turnTracePrefix,
1939
+ });
1940
+ if ('session' in outcome &&
1941
+ outcome.session !== latestSessionRef.current) {
1942
+ commitSession(outcome.session);
1943
+ }
1944
+ if (outcome.kind === 'completed') {
1945
+ latestTurnsRef.current += 1;
1946
+ setEntries((previous) => outcome.streamedAssistantEntryKey
1947
+ ? applyInkChatTraceUpdates(previous, [
1948
+ {
1949
+ kind: 'assistant',
1950
+ mode: 'replace',
1951
+ streamKey: outcome.streamedAssistantEntryKey,
1952
+ text: outcome.response,
1953
+ },
1954
+ ])
1955
+ : [
1956
+ ...previous,
1957
+ {
1958
+ kind: 'assistant',
1959
+ text: outcome.response,
1960
+ },
1961
+ ]);
1962
+ setStatus(outcome.delivery
1963
+ ? {
1964
+ kind: 'success',
1965
+ text: `Delivered over ${outcome.delivery.channel} to ${outcome.delivery.target}.`,
1966
+ }
1967
+ : outcome.deliveryError
1968
+ ? {
1969
+ kind: 'error',
1970
+ text: `Response saved locally, but delivery failed: ${outcome.deliveryError.message}`,
1971
+ }
1972
+ : null);
1973
+ }
1974
+ if (outcome.kind === 'failed') {
1975
+ if (outcome.recoveredSession) {
1976
+ commitSession(outcome.recoveredSession);
1977
+ }
1978
+ const queuedPrompts = promptQueueStateRef.current.prompts;
1979
+ const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
1980
+ pauseRequested: false,
1981
+ queuedPrompts,
1982
+ turnOutcome: 'failed',
1983
+ });
1984
+ applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
1985
+ const errorPresentation = resolveAssistantTurnErrorPresentation({
1986
+ error: outcome.error,
1987
+ restoredQueuedPromptCount: queuedPromptDisposition.kind === 'restore-composer'
1988
+ ? queuedPromptDisposition.restoredQueuedPromptCount
1989
+ : 0,
1990
+ });
1991
+ setEntries((previous) => [
1992
+ ...previous,
1993
+ errorPresentation.entry,
1994
+ ]);
1995
+ setStatus(errorPresentation.status);
1996
+ if (errorPresentation.persistTranscriptError) {
1997
+ void appendAssistantTranscriptEntries(input.input.vault, latestSessionRef.current.sessionId, [
1998
+ {
1999
+ kind: 'error',
2000
+ text: errorPresentation.entry.text,
2001
+ },
2002
+ ]).catch(() => { });
2003
+ }
2004
+ }
2005
+ if (outcome.kind === 'interrupted' && outcome.recoveredSession) {
2006
+ commitSession(outcome.recoveredSession);
2007
+ }
2008
+ activeTurnAbortControllerRef.current = null;
2009
+ setEntries((previous) => finalizePendingInkChatTraces(previous, turnTracePrefix));
2010
+ const pauseRequested = turnStateRef.current.pauseRequested;
2011
+ updateTurnState({
2012
+ kind: 'finish',
2013
+ });
2014
+ if (outcome.kind === 'interrupted') {
2015
+ const queuedPrompts = promptQueueStateRef.current.prompts;
2016
+ const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
2017
+ pauseRequested,
2018
+ queuedPrompts,
2019
+ turnOutcome: 'interrupted',
2020
+ });
2021
+ applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
2022
+ setStatus({
2023
+ kind: 'info',
2024
+ text: queuedPromptDisposition.kind === 'restore-composer' &&
2025
+ queuedPromptDisposition.restoredQueuedPromptCount > 0
2026
+ ? 'Paused current turn. Queued follow-ups are back in the composer.'
2027
+ : 'Paused current turn.',
2028
+ });
2029
+ return;
2030
+ }
2031
+ const queuedPrompts = promptQueueStateRef.current.prompts;
2032
+ const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
2033
+ pauseRequested,
2034
+ queuedPrompts,
2035
+ turnOutcome: outcome.kind,
2036
+ });
2037
+ if (queuedPromptDisposition.kind === 'restore-composer' &&
2038
+ outcome.kind === 'completed' &&
2039
+ pauseRequested) {
2040
+ applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
2041
+ setStatus({
2042
+ kind: 'info',
2043
+ text: queuedPromptDisposition.restoredQueuedPromptCount > 0
2044
+ ? 'Stopped after the current turn. Queued follow-ups are back in the composer.'
2045
+ : 'Stopped after the current turn.',
2046
+ });
2047
+ return;
2048
+ }
2049
+ if (outcome.kind === 'completed') {
2050
+ const nextQueuedPrompt = applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
2051
+ if (nextQueuedPrompt) {
2052
+ queueMicrotask(() => {
2053
+ startPromptTurn(nextQueuedPrompt);
2054
+ });
2055
+ }
2056
+ }
2057
+ })();
2058
+ };
2059
+ const openModelSwitcher = () => {
2060
+ const reasoningOptions = resolveAssistantCatalogReasoningOptions(modelCatalog.models[findAssistantModelOptionIndex(activeModel, modelCatalog.modelOptions)]);
2061
+ setModelSwitcherState({
2062
+ models: modelCatalog.models,
2063
+ mode: 'model',
2064
+ modelIndex: findAssistantModelOptionIndex(activeModel, modelCatalog.modelOptions),
2065
+ reasoningIndex: findAssistantReasoningOptionIndex(activeReasoningEffort, reasoningOptions),
2066
+ modelOptions: modelCatalog.modelOptions,
2067
+ reasoningOptions,
2068
+ });
2069
+ };
2070
+ const moveModelSwitcherSelection = (delta) => {
2071
+ setModelSwitcherState((previous) => {
2072
+ if (!previous) {
2073
+ return previous;
2074
+ }
2075
+ if (previous.mode === 'model') {
2076
+ const modelIndex = wrapPickerIndex(previous.modelIndex + delta, previous.modelOptions.length);
2077
+ const reasoningOptions = resolveAssistantCatalogReasoningOptions(previous.models[modelIndex]);
2078
+ return {
2079
+ ...previous,
2080
+ modelIndex,
2081
+ reasoningIndex: findAssistantReasoningOptionIndex(activeReasoningEffort, reasoningOptions),
2082
+ reasoningOptions,
2083
+ };
2084
+ }
2085
+ return {
2086
+ ...previous,
2087
+ reasoningIndex: wrapPickerIndex(previous.reasoningIndex + delta, previous.reasoningOptions.length),
2088
+ };
2089
+ });
2090
+ };
2091
+ const cancelModelSwitcher = () => {
2092
+ setModelSwitcherState((previous) => {
2093
+ if (!previous) {
2094
+ return previous;
2095
+ }
2096
+ if (previous.mode === 'reasoning') {
2097
+ return {
2098
+ ...previous,
2099
+ mode: 'model',
2100
+ };
2101
+ }
2102
+ return null;
2103
+ });
2104
+ };
2105
+ const applyModelSwitcherSelection = (selection) => {
2106
+ const nextModel = selection.modelOptions[selection.modelIndex]?.value ??
2107
+ activeModel ??
2108
+ null;
2109
+ const nextReasoningEffort = selection.reasoningOptions.length > 0
2110
+ ? selection.reasoningOptions[selection.reasoningIndex]?.value ??
2111
+ activeReasoningEffort ??
2112
+ 'medium'
2113
+ : null;
2114
+ const selectedLabel = [
2115
+ nextModel ?? 'the configured model',
2116
+ normalizeNullableString(nextReasoningEffort),
2117
+ ]
2118
+ .filter((value) => Boolean(value))
2119
+ .join(' ');
2120
+ setActiveSelection({
2121
+ activeModel: nextModel,
2122
+ activeReasoningEffort: nextReasoningEffort,
2123
+ });
2124
+ setModelSwitcherState(null);
2125
+ setStatus({
2126
+ kind: 'info',
2127
+ text: `Using ${selectedLabel}.`,
2128
+ });
2129
+ void (async () => {
2130
+ try {
2131
+ const updatedSession = await updateAssistantSessionOptions({
2132
+ vault: input.input.vault,
2133
+ sessionId: latestSessionRef.current.sessionId,
2134
+ providerOptions: {
2135
+ model: nextModel,
2136
+ reasoningEffort: nextReasoningEffort,
2137
+ },
2138
+ });
2139
+ commitSession(updatedSession);
2140
+ await saveAssistantOperatorDefaultsPatch(buildAssistantProviderDefaultsPatch({
2141
+ defaults: input.defaults,
2142
+ provider: updatedSession.provider,
2143
+ providerConfig: {
2144
+ ...updatedSession.providerOptions,
2145
+ model: nextModel,
2146
+ reasoningEffort: nextReasoningEffort,
2147
+ },
2148
+ }));
2149
+ }
2150
+ catch (error) {
2151
+ setStatus({
2152
+ kind: 'error',
2153
+ text: error instanceof Error && error.message.trim().length > 0
2154
+ ? `Using ${selectedLabel} for now, but failed to save it for later chats: ${error.message}`
2155
+ : `Using ${selectedLabel} for now, but failed to save it for later chats.`,
2156
+ });
2157
+ }
2158
+ })();
2159
+ };
2160
+ const confirmModelSwitcher = () => {
2161
+ if (!modelSwitcherState) {
2162
+ return;
2163
+ }
2164
+ if (modelSwitcherState.mode === 'model' &&
2165
+ modelSwitcherState.reasoningOptions.length > 0) {
2166
+ setModelSwitcherState({
2167
+ ...modelSwitcherState,
2168
+ mode: 'reasoning',
2169
+ });
2170
+ return;
2171
+ }
2172
+ applyModelSwitcherSelection(modelSwitcherState);
2173
+ };
2174
+ const requestPause = () => {
2175
+ if (turnStateRef.current.phase !== 'running' ||
2176
+ modelSwitcherState ||
2177
+ turnStateRef.current.pauseRequested ||
2178
+ !activeTurnAbortControllerRef.current) {
2179
+ return;
2180
+ }
2181
+ updateTurnState({
2182
+ kind: 'request-pause',
2183
+ });
2184
+ setStatus({
2185
+ kind: 'info',
2186
+ text: promptQueueStateRef.current.prompts.length > 0
2187
+ ? 'Pausing current turn. Queued follow-ups will return to the composer.'
2188
+ : 'Pausing current turn...',
2189
+ });
2190
+ activeTurnAbortControllerRef.current.abort();
2191
+ };
2192
+ useInput((_input, key) => {
2193
+ if (!key.escape) {
2194
+ return;
2195
+ }
2196
+ requestPause();
2197
+ }, {
2198
+ isActive: turnState.phase === 'running' && modelSwitcherState === null,
2199
+ });
2200
+ const submitPrompt = (rawValue, mode) => {
2201
+ const action = resolveChatSubmitAction(rawValue, {
2202
+ busy: turnState.phase === 'running',
2203
+ trigger: mode,
2204
+ });
2205
+ if (action.kind === 'ignore') {
2206
+ return 'keep';
2207
+ }
2208
+ if (action.kind === 'exit') {
2209
+ exit();
2210
+ return 'keep';
2211
+ }
2212
+ if (action.kind === 'session') {
2213
+ setStatus({
2214
+ kind: 'info',
2215
+ text: `session ${latestSessionRef.current.sessionId}`,
2216
+ });
2217
+ return 'keep';
2218
+ }
2219
+ if (action.kind === 'model') {
2220
+ setStatus(null);
2221
+ openModelSwitcher();
2222
+ return 'clear';
2223
+ }
2224
+ if (action.kind === 'queue') {
2225
+ queuePrompt(action.prompt);
2226
+ return 'clear';
2227
+ }
2228
+ startPromptTurn(action.prompt);
2229
+ return shouldClearComposerForSubmitAction(action) ? 'clear' : 'keep';
2230
+ };
2231
+ React.useEffect(() => {
2232
+ if (bootstrappedRef.current) {
2233
+ return;
2234
+ }
2235
+ bootstrappedRef.current = true;
2236
+ if (initialPromptRef.current) {
2237
+ submitPrompt(initialPromptRef.current, 'enter');
2238
+ }
2239
+ }, []);
2240
+ const bindingSummary = formatSessionBinding(session);
2241
+ const metadataBadges = resolveChatMetadataBadges({
2242
+ baseUrl: session.providerOptions.baseUrl,
2243
+ provider: session.provider,
2244
+ model: activeModel ?? session.providerOptions.model ?? input.codexDisplay.model,
2245
+ reasoningEffort: activeReasoningEffort ?? input.codexDisplay.reasoningEffort,
2246
+ }, input.redactedVault);
2247
+ return {
2248
+ activeModel,
2249
+ activeReasoningEffort,
2250
+ bindingSummary,
2251
+ busy: turnState.phase === 'running',
2252
+ cancelModelSwitcher,
2253
+ composerValue,
2254
+ confirmModelSwitcher,
2255
+ editLastQueuedPrompt,
2256
+ entries,
2257
+ lastQueuedPrompt: promptQueueState.prompts.at(-1) ?? null,
2258
+ latestSessionRef,
2259
+ latestTurnsRef,
2260
+ metadataBadges,
2261
+ modelSwitcherState,
2262
+ moveModelSwitcherSelection,
2263
+ queuedPromptCount: promptQueueState.prompts.length,
2264
+ session,
2265
+ setComposerValue,
2266
+ status,
2267
+ submitPrompt,
2268
+ };
2269
+ }
2270
+ export async function runAssistantChatWithInk(input) {
2271
+ const startedAt = new Date().toISOString();
2272
+ const defaults = await resolveAssistantOperatorDefaults();
2273
+ const themeBaseline = captureAssistantInkThemeBaseline();
2274
+ const resolved = await openAssistantConversation(input);
2275
+ const selectedProviderDefaults = resolveAssistantProviderDefaults(defaults, resolved.session.provider);
2276
+ const transcriptEntries = await listAssistantTranscriptEntries(input.vault, resolved.session.sessionId);
2277
+ const redactedVault = redactAssistantDisplayPath(input.vault);
2278
+ const codexDisplay = await resolveCodexDisplayOptions({
2279
+ model: input.model ??
2280
+ selectedProviderDefaults?.model ??
2281
+ resolved.session.providerOptions.model,
2282
+ profile: input.profile ??
2283
+ selectedProviderDefaults?.profile ??
2284
+ resolved.session.providerOptions.profile,
2285
+ });
2286
+ const inkInput = resolveAssistantInkInputAdapter();
2287
+ if (!inkInput.stdin) {
2288
+ throw new Error('Murph chat requires interactive terminal input. process.stdin does not support raw mode, and Murph could not open the controlling terminal for Ink input.');
2289
+ }
2290
+ const inkStdin = inkInput.stdin;
2291
+ return await new Promise((resolve, reject) => {
2292
+ let settled = false;
2293
+ let instance = null;
2294
+ const resolveOnce = (result) => {
2295
+ if (settled) {
2296
+ return;
2297
+ }
2298
+ settled = true;
2299
+ inkInput.close();
2300
+ resolve(result);
2301
+ };
2302
+ const rejectOnce = (error) => {
2303
+ if (settled) {
2304
+ return;
2305
+ }
2306
+ settled = true;
2307
+ inkInput.close();
2308
+ reject(error);
2309
+ };
2310
+ const App = () => {
2311
+ const createElement = React.createElement;
2312
+ const [theme, setTheme] = React.useState(() => themeBaseline.theme);
2313
+ const controller = useAssistantChatController({
2314
+ codexDisplay,
2315
+ defaults,
2316
+ input,
2317
+ redactedVault,
2318
+ resolvedSession: resolved.session,
2319
+ selectedProviderDefaults,
2320
+ transcriptEntries,
2321
+ });
2322
+ React.useEffect(() => {
2323
+ if (process.platform !== 'darwin') {
2324
+ return undefined;
2325
+ }
2326
+ const refreshTimer = setInterval(() => {
2327
+ setTheme((currentTheme) => {
2328
+ const nextTheme = resolveAssistantInkThemeForOpenChat({
2329
+ currentMode: currentTheme.mode,
2330
+ initialAppleInterfaceStyle: themeBaseline.initialAppleInterfaceStyle,
2331
+ initialColorFgbg: themeBaseline.initialColorFgbg,
2332
+ });
2333
+ return nextTheme.mode === currentTheme.mode
2334
+ ? currentTheme
2335
+ : nextTheme;
2336
+ });
2337
+ }, ASSISTANT_INK_THEME_REFRESH_INTERVAL_MS);
2338
+ return () => {
2339
+ clearInterval(refreshTimer);
2340
+ };
2341
+ }, []);
2342
+ React.useEffect(() => () => {
2343
+ resolveOnce(assistantChatResultSchema.parse({
2344
+ vault: redactedVault,
2345
+ startedAt,
2346
+ stoppedAt: new Date().toISOString(),
2347
+ turns: controller.latestTurnsRef.current,
2348
+ session: redactAssistantSessionForDisplay(controller.latestSessionRef.current),
2349
+ }));
2350
+ }, []);
2351
+ return createElement(AssistantInkThemeContext.Provider, {
2352
+ value: theme,
2353
+ }, createElement(Box, {
2354
+ flexDirection: 'column',
2355
+ paddingX: ASSISTANT_CHAT_VIEW_PADDING_X,
2356
+ paddingY: 1,
2357
+ width: '100%',
2358
+ }, createElement(ChatTranscriptFeed, {
2359
+ bindingSummary: controller.bindingSummary,
2360
+ busy: controller.busy,
2361
+ entries: controller.entries,
2362
+ sessionId: controller.session.sessionId,
2363
+ }), createElement(Box, {
2364
+ flexDirection: 'column',
2365
+ width: '100%',
2366
+ }, createElement(ChatStatus, {
2367
+ busy: shouldShowBusyStatus({
2368
+ busy: controller.busy,
2369
+ entries: controller.entries,
2370
+ }),
2371
+ status: controller.status,
2372
+ }), createElement(QueuedFollowUpStatus, {
2373
+ latestPrompt: controller.lastQueuedPrompt,
2374
+ queuedPromptCount: controller.queuedPromptCount,
2375
+ }), controller.modelSwitcherState
2376
+ ? createElement(ModelSwitcher, {
2377
+ currentModel: controller.activeModel,
2378
+ currentReasoningEffort: controller.activeReasoningEffort,
2379
+ mode: controller.modelSwitcherState.mode,
2380
+ modelIndex: controller.modelSwitcherState.modelIndex,
2381
+ modelOptions: controller.modelSwitcherState.modelOptions,
2382
+ onCancel: controller.cancelModelSwitcher,
2383
+ onConfirm: controller.confirmModelSwitcher,
2384
+ onMove: controller.moveModelSwitcherSelection,
2385
+ reasoningIndex: controller.modelSwitcherState.reasoningIndex,
2386
+ reasoningOptions: controller.modelSwitcherState.reasoningOptions,
2387
+ })
2388
+ : null, createElement(ChatComposer, {
2389
+ entryCount: controller.entries.length,
2390
+ modelSwitcherActive: controller.modelSwitcherState !== null,
2391
+ onChange: controller.setComposerValue,
2392
+ onEditLastQueuedPrompt: controller.editLastQueuedPrompt,
2393
+ onSubmit: controller.submitPrompt,
2394
+ value: controller.composerValue,
2395
+ }), createElement(ChatFooter, {
2396
+ badges: controller.metadataBadges,
2397
+ }))));
2398
+ };
2399
+ try {
2400
+ instance = render(React.createElement(App), {
2401
+ stderr: process.stderr,
2402
+ stdin: inkStdin,
2403
+ stdout: process.stderr,
2404
+ patchConsole: false,
2405
+ });
2406
+ void instance.waitUntilExit().catch(rejectOnce);
2407
+ }
2408
+ catch (error) {
2409
+ rejectOnce(error);
2410
+ return;
2411
+ }
2412
+ if (!instance) {
2413
+ rejectOnce(new Error('Ink chat failed to initialize.'));
2414
+ }
2415
+ });
2416
+ }
2417
+ //# sourceMappingURL=ink.js.map