reigncode-app 1.3.2

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 (300) hide show
  1. package/AGENTS.md +30 -0
  2. package/Dockerfile +21 -0
  3. package/README.md +51 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/AGENTS.md +226 -0
  7. package/e2e/actions.ts +1018 -0
  8. package/e2e/app/home.spec.ts +24 -0
  9. package/e2e/app/navigation.spec.ts +10 -0
  10. package/e2e/app/palette.spec.ts +20 -0
  11. package/e2e/app/server-default.spec.ts +58 -0
  12. package/e2e/app/session.spec.ts +16 -0
  13. package/e2e/app/titlebar-history.spec.ts +120 -0
  14. package/e2e/commands/input-focus.spec.ts +15 -0
  15. package/e2e/commands/panels.spec.ts +33 -0
  16. package/e2e/commands/tab-close.spec.ts +32 -0
  17. package/e2e/files/file-open.spec.ts +31 -0
  18. package/e2e/files/file-tree.spec.ts +56 -0
  19. package/e2e/files/file-viewer.spec.ts +156 -0
  20. package/e2e/fixtures.ts +154 -0
  21. package/e2e/models/model-picker.spec.ts +48 -0
  22. package/e2e/models/models-visibility.spec.ts +61 -0
  23. package/e2e/projects/project-edit.spec.ts +43 -0
  24. package/e2e/projects/projects-close.spec.ts +54 -0
  25. package/e2e/projects/projects-switch.spec.ts +116 -0
  26. package/e2e/projects/workspace-new-session.spec.ts +94 -0
  27. package/e2e/projects/workspaces.spec.ts +375 -0
  28. package/e2e/prompt/context.spec.ts +95 -0
  29. package/e2e/prompt/prompt-async.spec.ts +76 -0
  30. package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
  31. package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
  32. package/e2e/prompt/prompt-history.spec.ts +184 -0
  33. package/e2e/prompt/prompt-mention.spec.ts +26 -0
  34. package/e2e/prompt/prompt-multiline.spec.ts +24 -0
  35. package/e2e/prompt/prompt-shell.spec.ts +62 -0
  36. package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
  37. package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
  38. package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
  39. package/e2e/prompt/prompt.spec.ts +55 -0
  40. package/e2e/selectors.ts +75 -0
  41. package/e2e/session/session-child-navigation.spec.ts +37 -0
  42. package/e2e/session/session-composer-dock.spec.ts +530 -0
  43. package/e2e/session/session-model-persistence.spec.ts +359 -0
  44. package/e2e/session/session-review.spec.ts +426 -0
  45. package/e2e/session/session-undo-redo.spec.ts +233 -0
  46. package/e2e/session/session.spec.ts +174 -0
  47. package/e2e/settings/settings-keybinds.spec.ts +389 -0
  48. package/e2e/settings/settings-models.spec.ts +122 -0
  49. package/e2e/settings/settings-providers.spec.ts +136 -0
  50. package/e2e/settings/settings.spec.ts +519 -0
  51. package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
  52. package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
  53. package/e2e/sidebar/sidebar.spec.ts +40 -0
  54. package/e2e/status/status-popover.spec.ts +94 -0
  55. package/e2e/terminal/terminal-init.spec.ts +28 -0
  56. package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
  57. package/e2e/terminal/terminal-tabs.spec.ts +168 -0
  58. package/e2e/terminal/terminal.spec.ts +18 -0
  59. package/e2e/thinking-level.spec.ts +25 -0
  60. package/e2e/tsconfig.json +9 -0
  61. package/e2e/utils.ts +63 -0
  62. package/happydom.ts +75 -0
  63. package/index.html +23 -0
  64. package/package.json +77 -0
  65. package/playwright.config.ts +45 -0
  66. package/public/_headers +17 -0
  67. package/public/oc-theme-preload.js +35 -0
  68. package/script/e2e-local.ts +180 -0
  69. package/src/addons/serialize.test.ts +319 -0
  70. package/src/addons/serialize.ts +634 -0
  71. package/src/app.tsx +308 -0
  72. package/src/components/debug-bar.tsx +443 -0
  73. package/src/components/dialog-connect-provider.tsx +617 -0
  74. package/src/components/dialog-custom-provider-form.ts +158 -0
  75. package/src/components/dialog-custom-provider.test.ts +80 -0
  76. package/src/components/dialog-custom-provider.tsx +329 -0
  77. package/src/components/dialog-edit-project.tsx +255 -0
  78. package/src/components/dialog-fork.tsx +108 -0
  79. package/src/components/dialog-manage-models.tsx +101 -0
  80. package/src/components/dialog-release-notes.tsx +144 -0
  81. package/src/components/dialog-select-directory.tsx +392 -0
  82. package/src/components/dialog-select-file.tsx +466 -0
  83. package/src/components/dialog-select-mcp.tsx +107 -0
  84. package/src/components/dialog-select-model-unpaid.tsx +137 -0
  85. package/src/components/dialog-select-model.tsx +220 -0
  86. package/src/components/dialog-select-provider.tsx +86 -0
  87. package/src/components/dialog-select-server.tsx +649 -0
  88. package/src/components/dialog-settings.tsx +73 -0
  89. package/src/components/file-tree.test.ts +78 -0
  90. package/src/components/file-tree.tsx +507 -0
  91. package/src/components/link.tsx +26 -0
  92. package/src/components/model-tooltip.tsx +91 -0
  93. package/src/components/prompt-input/attachments.test.ts +44 -0
  94. package/src/components/prompt-input/attachments.ts +201 -0
  95. package/src/components/prompt-input/build-request-parts.test.ts +312 -0
  96. package/src/components/prompt-input/build-request-parts.ts +175 -0
  97. package/src/components/prompt-input/context-items.tsx +88 -0
  98. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  99. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  100. package/src/components/prompt-input/editor-dom.ts +148 -0
  101. package/src/components/prompt-input/files.ts +66 -0
  102. package/src/components/prompt-input/history.test.ts +153 -0
  103. package/src/components/prompt-input/history.ts +256 -0
  104. package/src/components/prompt-input/image-attachments.tsx +58 -0
  105. package/src/components/prompt-input/paste.ts +24 -0
  106. package/src/components/prompt-input/placeholder.test.ts +48 -0
  107. package/src/components/prompt-input/placeholder.ts +15 -0
  108. package/src/components/prompt-input/slash-popover.tsx +141 -0
  109. package/src/components/prompt-input/submit.test.ts +346 -0
  110. package/src/components/prompt-input/submit.ts +579 -0
  111. package/src/components/prompt-input.tsx +1595 -0
  112. package/src/components/server/server-row.tsx +130 -0
  113. package/src/components/session/index.ts +5 -0
  114. package/src/components/session/session-context-breakdown.test.ts +61 -0
  115. package/src/components/session/session-context-breakdown.ts +132 -0
  116. package/src/components/session/session-context-format.ts +20 -0
  117. package/src/components/session/session-context-metrics.test.ts +101 -0
  118. package/src/components/session/session-context-metrics.ts +82 -0
  119. package/src/components/session/session-context-tab.tsx +339 -0
  120. package/src/components/session/session-header.tsx +486 -0
  121. package/src/components/session/session-new-view.tsx +91 -0
  122. package/src/components/session/session-sortable-tab.tsx +70 -0
  123. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  124. package/src/components/session-context-usage.tsx +122 -0
  125. package/src/components/settings-general.tsx +585 -0
  126. package/src/components/settings-keybinds.tsx +453 -0
  127. package/src/components/settings-list.tsx +5 -0
  128. package/src/components/settings-models.tsx +137 -0
  129. package/src/components/settings-providers.tsx +251 -0
  130. package/src/components/status-popover.tsx +419 -0
  131. package/src/components/terminal.tsx +653 -0
  132. package/src/components/titlebar-history.test.ts +63 -0
  133. package/src/components/titlebar-history.ts +57 -0
  134. package/src/components/titlebar.tsx +312 -0
  135. package/src/constants/file-picker.ts +89 -0
  136. package/src/context/command-keybind.test.ts +69 -0
  137. package/src/context/command.test.ts +25 -0
  138. package/src/context/command.tsx +437 -0
  139. package/src/context/comments.test.ts +186 -0
  140. package/src/context/comments.tsx +243 -0
  141. package/src/context/file/content-cache.ts +88 -0
  142. package/src/context/file/path.test.ts +360 -0
  143. package/src/context/file/path.ts +151 -0
  144. package/src/context/file/tree-store.ts +170 -0
  145. package/src/context/file/types.ts +41 -0
  146. package/src/context/file/view-cache.ts +146 -0
  147. package/src/context/file/watcher.test.ts +149 -0
  148. package/src/context/file/watcher.ts +53 -0
  149. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  150. package/src/context/file.tsx +280 -0
  151. package/src/context/global-sdk.tsx +232 -0
  152. package/src/context/global-sync/bootstrap.ts +206 -0
  153. package/src/context/global-sync/child-store.test.ts +38 -0
  154. package/src/context/global-sync/child-store.ts +281 -0
  155. package/src/context/global-sync/event-reducer.test.ts +552 -0
  156. package/src/context/global-sync/event-reducer.ts +359 -0
  157. package/src/context/global-sync/eviction.ts +28 -0
  158. package/src/context/global-sync/queue.ts +83 -0
  159. package/src/context/global-sync/session-cache.test.ts +102 -0
  160. package/src/context/global-sync/session-cache.ts +62 -0
  161. package/src/context/global-sync/session-load.ts +25 -0
  162. package/src/context/global-sync/session-prefetch.test.ts +96 -0
  163. package/src/context/global-sync/session-prefetch.ts +100 -0
  164. package/src/context/global-sync/session-trim.test.ts +59 -0
  165. package/src/context/global-sync/session-trim.ts +56 -0
  166. package/src/context/global-sync/types.ts +133 -0
  167. package/src/context/global-sync/utils.ts +25 -0
  168. package/src/context/global-sync.test.ts +122 -0
  169. package/src/context/global-sync.tsx +408 -0
  170. package/src/context/highlights.tsx +233 -0
  171. package/src/context/language.tsx +248 -0
  172. package/src/context/layout-scroll.test.ts +64 -0
  173. package/src/context/layout-scroll.ts +126 -0
  174. package/src/context/layout.test.ts +69 -0
  175. package/src/context/layout.tsx +937 -0
  176. package/src/context/local.tsx +422 -0
  177. package/src/context/model-variant.test.ts +86 -0
  178. package/src/context/model-variant.ts +52 -0
  179. package/src/context/models.tsx +163 -0
  180. package/src/context/notification.tsx +373 -0
  181. package/src/context/permission-auto-respond.test.ts +102 -0
  182. package/src/context/permission-auto-respond.ts +51 -0
  183. package/src/context/permission.tsx +277 -0
  184. package/src/context/platform.tsx +99 -0
  185. package/src/context/prompt.tsx +297 -0
  186. package/src/context/sdk.tsx +49 -0
  187. package/src/context/server.tsx +295 -0
  188. package/src/context/settings.tsx +241 -0
  189. package/src/context/sync-optimistic.test.ts +123 -0
  190. package/src/context/sync.tsx +618 -0
  191. package/src/context/terminal-title.ts +51 -0
  192. package/src/context/terminal.test.ts +82 -0
  193. package/src/context/terminal.tsx +437 -0
  194. package/src/entry.tsx +144 -0
  195. package/src/env.d.ts +18 -0
  196. package/src/hooks/use-providers.ts +44 -0
  197. package/src/i18n/ar.ts +855 -0
  198. package/src/i18n/br.ts +867 -0
  199. package/src/i18n/bs.ts +943 -0
  200. package/src/i18n/da.ts +937 -0
  201. package/src/i18n/de.ts +879 -0
  202. package/src/i18n/en.ts +948 -0
  203. package/src/i18n/es.ts +950 -0
  204. package/src/i18n/fr.ts +878 -0
  205. package/src/i18n/ja.ts +861 -0
  206. package/src/i18n/ko.ts +860 -0
  207. package/src/i18n/no.ts +944 -0
  208. package/src/i18n/parity.test.ts +32 -0
  209. package/src/i18n/pl.ts +865 -0
  210. package/src/i18n/ru.ts +946 -0
  211. package/src/i18n/th.ts +933 -0
  212. package/src/i18n/tr.ts +952 -0
  213. package/src/i18n/zh.ts +930 -0
  214. package/src/i18n/zht.ts +925 -0
  215. package/src/index.css +29 -0
  216. package/src/index.ts +6 -0
  217. package/src/pages/directory-layout.tsx +88 -0
  218. package/src/pages/error.tsx +327 -0
  219. package/src/pages/home.tsx +131 -0
  220. package/src/pages/layout/deep-links.ts +50 -0
  221. package/src/pages/layout/helpers.test.ts +211 -0
  222. package/src/pages/layout/helpers.ts +98 -0
  223. package/src/pages/layout/inline-editor.tsx +126 -0
  224. package/src/pages/layout/sidebar-items.tsx +437 -0
  225. package/src/pages/layout/sidebar-project.tsx +384 -0
  226. package/src/pages/layout/sidebar-shell.tsx +125 -0
  227. package/src/pages/layout/sidebar-workspace.tsx +504 -0
  228. package/src/pages/layout.tsx +2509 -0
  229. package/src/pages/session/composer/index.ts +2 -0
  230. package/src/pages/session/composer/session-composer-region.tsx +255 -0
  231. package/src/pages/session/composer/session-composer-state.test.ts +128 -0
  232. package/src/pages/session/composer/session-composer-state.ts +249 -0
  233. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  234. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  235. package/src/pages/session/composer/session-question-dock.tsx +449 -0
  236. package/src/pages/session/composer/session-request-tree.ts +52 -0
  237. package/src/pages/session/composer/session-revert-dock.tsx +99 -0
  238. package/src/pages/session/composer/session-todo-dock.tsx +330 -0
  239. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  240. package/src/pages/session/file-tab-scroll.ts +67 -0
  241. package/src/pages/session/file-tabs.tsx +456 -0
  242. package/src/pages/session/handoff.ts +36 -0
  243. package/src/pages/session/helpers.test.ts +181 -0
  244. package/src/pages/session/helpers.ts +198 -0
  245. package/src/pages/session/message-gesture.test.ts +62 -0
  246. package/src/pages/session/message-gesture.ts +21 -0
  247. package/src/pages/session/message-id-from-hash.ts +6 -0
  248. package/src/pages/session/message-timeline.tsx +1013 -0
  249. package/src/pages/session/review-tab.tsx +170 -0
  250. package/src/pages/session/session-layout.ts +20 -0
  251. package/src/pages/session/session-model-helpers.test.ts +51 -0
  252. package/src/pages/session/session-model-helpers.ts +16 -0
  253. package/src/pages/session/session-side-panel.tsx +453 -0
  254. package/src/pages/session/terminal-label.ts +16 -0
  255. package/src/pages/session/terminal-panel.test.ts +25 -0
  256. package/src/pages/session/terminal-panel.tsx +326 -0
  257. package/src/pages/session/use-session-commands.tsx +495 -0
  258. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  259. package/src/pages/session/use-session-hash-scroll.ts +197 -0
  260. package/src/pages/session.tsx +1841 -0
  261. package/src/sst-env.d.ts +12 -0
  262. package/src/testing/model-selection.ts +80 -0
  263. package/src/testing/prompt.ts +56 -0
  264. package/src/testing/session-composer.ts +84 -0
  265. package/src/testing/terminal.ts +118 -0
  266. package/src/theme-preload.test.ts +46 -0
  267. package/src/utils/agent.ts +23 -0
  268. package/src/utils/aim.ts +138 -0
  269. package/src/utils/base64.ts +10 -0
  270. package/src/utils/comment-note.ts +88 -0
  271. package/src/utils/id.ts +99 -0
  272. package/src/utils/notification-click.test.ts +27 -0
  273. package/src/utils/notification-click.ts +13 -0
  274. package/src/utils/persist.test.ts +115 -0
  275. package/src/utils/persist.ts +476 -0
  276. package/src/utils/prompt.test.ts +44 -0
  277. package/src/utils/prompt.ts +203 -0
  278. package/src/utils/runtime-adapters.test.ts +62 -0
  279. package/src/utils/runtime-adapters.ts +39 -0
  280. package/src/utils/same.ts +6 -0
  281. package/src/utils/scoped-cache.test.ts +69 -0
  282. package/src/utils/scoped-cache.ts +104 -0
  283. package/src/utils/server-errors.test.ts +131 -0
  284. package/src/utils/server-errors.ts +80 -0
  285. package/src/utils/server-health.test.ts +123 -0
  286. package/src/utils/server-health.ts +91 -0
  287. package/src/utils/server.ts +22 -0
  288. package/src/utils/solid-dnd.tsx +49 -0
  289. package/src/utils/sound.ts +117 -0
  290. package/src/utils/terminal-writer.test.ts +64 -0
  291. package/src/utils/terminal-writer.ts +65 -0
  292. package/src/utils/time.ts +22 -0
  293. package/src/utils/uuid.test.ts +78 -0
  294. package/src/utils/uuid.ts +12 -0
  295. package/src/utils/worktree.test.ts +46 -0
  296. package/src/utils/worktree.ts +73 -0
  297. package/sst-env.d.ts +10 -0
  298. package/tsconfig.json +26 -0
  299. package/vite.config.ts +15 -0
  300. package/vite.js +26 -0
@@ -0,0 +1,1595 @@
1
+ import { useFilteredList } from "@reign-labs/ui/hooks"
2
+ import { useSpring } from "@reign-labs/ui/motion-spring"
3
+ import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
4
+ import { createStore } from "solid-js/store"
5
+ import { useLocal } from "@/context/local"
6
+ import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
7
+ import {
8
+ ContentPart,
9
+ DEFAULT_PROMPT,
10
+ isPromptEqual,
11
+ Prompt,
12
+ usePrompt,
13
+ ImageAttachmentPart,
14
+ AgentPart,
15
+ FileAttachmentPart,
16
+ } from "@/context/prompt"
17
+ import { useLayout } from "@/context/layout"
18
+ import { useSDK } from "@/context/sdk"
19
+ import { useSync } from "@/context/sync"
20
+ import { useComments } from "@/context/comments"
21
+ import { Button } from "@reign-labs/ui/button"
22
+ import { DockShellForm, DockTray } from "@reign-labs/ui/dock-surface"
23
+ import { Icon } from "@reign-labs/ui/icon"
24
+ import { ProviderIcon } from "@reign-labs/ui/provider-icon"
25
+ import { Tooltip, TooltipKeybind } from "@reign-labs/ui/tooltip"
26
+ import { IconButton } from "@reign-labs/ui/icon-button"
27
+ import { Select } from "@reign-labs/ui/select"
28
+ import { useDialog } from "@reign-labs/ui/context/dialog"
29
+ import { ModelSelectorPopover } from "@/components/dialog-select-model"
30
+ import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
31
+ import { useProviders } from "@/hooks/use-providers"
32
+ import { useCommand } from "@/context/command"
33
+ import { Persist, persisted } from "@/utils/persist"
34
+ import { usePermission } from "@/context/permission"
35
+ import { useLanguage } from "@/context/language"
36
+ import { usePlatform } from "@/context/platform"
37
+ import { useSessionLayout } from "@/pages/session/session-layout"
38
+ import { createSessionTabs } from "@/pages/session/helpers"
39
+ import { promptEnabled, promptProbe } from "@/testing/prompt"
40
+ import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
41
+ import { createPromptAttachments } from "./prompt-input/attachments"
42
+ import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
43
+ import {
44
+ canNavigateHistoryAtCursor,
45
+ navigatePromptHistory,
46
+ prependHistoryEntry,
47
+ type PromptHistoryComment,
48
+ type PromptHistoryEntry,
49
+ type PromptHistoryStoredEntry,
50
+ promptLength,
51
+ } from "./prompt-input/history"
52
+ import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
53
+ import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
54
+ import { PromptContextItems } from "./prompt-input/context-items"
55
+ import { PromptImageAttachments } from "./prompt-input/image-attachments"
56
+ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
57
+ import { promptPlaceholder } from "./prompt-input/placeholder"
58
+ import { ImagePreview } from "@reign-labs/ui/image-preview"
59
+
60
+ interface PromptInputProps {
61
+ class?: string
62
+ ref?: (el: HTMLDivElement) => void
63
+ newSessionWorktree?: string
64
+ onNewSessionWorktreeReset?: () => void
65
+ edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
66
+ onEditLoaded?: () => void
67
+ shouldQueue?: () => boolean
68
+ onQueue?: (draft: FollowupDraft) => void
69
+ onAbort?: () => void
70
+ onSubmit?: () => void
71
+ }
72
+
73
+ const EXAMPLES = [
74
+ "prompt.example.1",
75
+ "prompt.example.2",
76
+ "prompt.example.3",
77
+ "prompt.example.4",
78
+ "prompt.example.5",
79
+ "prompt.example.6",
80
+ "prompt.example.7",
81
+ "prompt.example.8",
82
+ "prompt.example.9",
83
+ "prompt.example.10",
84
+ "prompt.example.11",
85
+ "prompt.example.12",
86
+ "prompt.example.13",
87
+ "prompt.example.14",
88
+ "prompt.example.15",
89
+ "prompt.example.16",
90
+ "prompt.example.17",
91
+ "prompt.example.18",
92
+ "prompt.example.19",
93
+ "prompt.example.20",
94
+ "prompt.example.21",
95
+ "prompt.example.22",
96
+ "prompt.example.23",
97
+ "prompt.example.24",
98
+ "prompt.example.25",
99
+ ] as const
100
+
101
+ const NON_EMPTY_TEXT = /[^\s\u200B]/
102
+
103
+ export const PromptInput: Component<PromptInputProps> = (props) => {
104
+ const sdk = useSDK()
105
+ const sync = useSync()
106
+ const local = useLocal()
107
+ const files = useFile()
108
+ const prompt = usePrompt()
109
+ const layout = useLayout()
110
+ const comments = useComments()
111
+ const dialog = useDialog()
112
+ const providers = useProviders()
113
+ const command = useCommand()
114
+ const permission = usePermission()
115
+ const language = useLanguage()
116
+ const platform = usePlatform()
117
+ const { params, tabs, view } = useSessionLayout()
118
+ let editorRef!: HTMLDivElement
119
+ let fileInputRef: HTMLInputElement | undefined
120
+ let scrollRef!: HTMLDivElement
121
+ let slashPopoverRef!: HTMLDivElement
122
+
123
+ const mirror = { input: false }
124
+ const inset = 56
125
+ const space = `${inset}px`
126
+
127
+ const scrollCursorIntoView = () => {
128
+ const container = scrollRef
129
+ const selection = window.getSelection()
130
+ if (!container || !selection || selection.rangeCount === 0) return
131
+
132
+ const range = selection.getRangeAt(0)
133
+ if (!editorRef.contains(range.startContainer)) return
134
+
135
+ const cursor = getCursorPosition(editorRef)
136
+ const length = promptLength(prompt.current().filter((part) => part.type !== "image"))
137
+ if (cursor >= length) {
138
+ container.scrollTop = container.scrollHeight
139
+ return
140
+ }
141
+
142
+ const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect()
143
+ if (!rect.height) return
144
+
145
+ const containerRect = container.getBoundingClientRect()
146
+ const top = rect.top - containerRect.top + container.scrollTop
147
+ const bottom = rect.bottom - containerRect.top + container.scrollTop
148
+ const padding = 12
149
+
150
+ if (top < container.scrollTop + padding) {
151
+ container.scrollTop = Math.max(0, top - padding)
152
+ return
153
+ }
154
+
155
+ if (bottom > container.scrollTop + container.clientHeight - inset) {
156
+ container.scrollTop = bottom - container.clientHeight + inset
157
+ }
158
+ }
159
+
160
+ const queueScroll = (count = 2) => {
161
+ requestAnimationFrame(() => {
162
+ scrollCursorIntoView()
163
+ if (count > 1) queueScroll(count - 1)
164
+ })
165
+ }
166
+
167
+ const activeFileTab = createSessionTabs({
168
+ tabs,
169
+ pathFromTab: files.pathFromTab,
170
+ normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab),
171
+ }).activeFileTab
172
+
173
+ const commentInReview = (path: string) => {
174
+ const sessionID = params.id
175
+ if (!sessionID) return false
176
+
177
+ const diffs = sync.data.session_diff[sessionID]
178
+ if (!diffs) return false
179
+ return diffs.some((diff) => diff.file === path)
180
+ }
181
+
182
+ const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => {
183
+ if (!item.commentID) return
184
+
185
+ const focus = { file: item.path, id: item.commentID }
186
+ comments.setActive(focus)
187
+
188
+ const queueCommentFocus = (attempts = 6) => {
189
+ const schedule = (left: number) => {
190
+ requestAnimationFrame(() => {
191
+ comments.setFocus({ ...focus })
192
+ if (left <= 0) return
193
+ requestAnimationFrame(() => {
194
+ const current = comments.focus()
195
+ if (!current) return
196
+ if (current.file !== focus.file || current.id !== focus.id) return
197
+ schedule(left - 1)
198
+ })
199
+ })
200
+ }
201
+
202
+ schedule(attempts)
203
+ }
204
+
205
+ const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
206
+ if (wantsReview) {
207
+ if (!view().reviewPanel.opened()) view().reviewPanel.open()
208
+ layout.fileTree.setTab("changes")
209
+ tabs().setActive("review")
210
+ queueCommentFocus()
211
+ return
212
+ }
213
+
214
+ if (!view().reviewPanel.opened()) view().reviewPanel.open()
215
+ layout.fileTree.setTab("all")
216
+ const tab = files.tab(item.path)
217
+ tabs().open(tab)
218
+ tabs().setActive(tab)
219
+ Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
220
+ }
221
+
222
+ const recent = createMemo(() => {
223
+ const all = tabs().all()
224
+ const active = activeFileTab()
225
+ const order = active ? [active, ...all.filter((x) => x !== active)] : all
226
+ const seen = new Set<string>()
227
+ const paths: string[] = []
228
+
229
+ for (const tab of order) {
230
+ const path = files.pathFromTab(tab)
231
+ if (!path) continue
232
+ if (seen.has(path)) continue
233
+ seen.add(path)
234
+ paths.push(path)
235
+ }
236
+
237
+ return paths
238
+ })
239
+ const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
240
+ const status = createMemo(
241
+ () =>
242
+ sync.data.session_status[params.id ?? ""] ?? {
243
+ type: "idle",
244
+ },
245
+ )
246
+ const working = createMemo(() => status()?.type !== "idle")
247
+ const tip = () => {
248
+ if (working()) {
249
+ return (
250
+ <div class="flex items-center gap-2">
251
+ <span>{language.t("prompt.action.stop")}</span>
252
+ <span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
253
+ </div>
254
+ )
255
+ }
256
+
257
+ return (
258
+ <div class="flex items-center gap-2">
259
+ <span>{language.t("prompt.action.send")}</span>
260
+ <Icon name="enter" size="small" class="text-icon-base" />
261
+ </div>
262
+ )
263
+ }
264
+ const imageAttachments = createMemo(() =>
265
+ prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
266
+ )
267
+
268
+ const [store, setStore] = createStore<{
269
+ popover: "at" | "slash" | null
270
+ historyIndex: number
271
+ savedPrompt: PromptHistoryEntry | null
272
+ placeholder: number
273
+ draggingType: "image" | "@mention" | null
274
+ mode: "normal" | "shell"
275
+ applyingHistory: boolean
276
+ }>({
277
+ popover: null,
278
+ historyIndex: -1,
279
+ savedPrompt: null as PromptHistoryEntry | null,
280
+ placeholder: Math.floor(Math.random() * EXAMPLES.length),
281
+ draggingType: null,
282
+ mode: "normal",
283
+ applyingHistory: false,
284
+ })
285
+
286
+ const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
287
+ const motion = (value: number) => ({
288
+ opacity: value,
289
+ transform: `scale(${0.95 + value * 0.05})`,
290
+ filter: `blur(${(1 - value) * 2}px)`,
291
+ "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
292
+ })
293
+ const buttons = createMemo(() => motion(buttonsSpring()))
294
+ const shell = createMemo(() => motion(1 - buttonsSpring()))
295
+ const control = createMemo(() => ({ height: "28px", ...buttons() }))
296
+
297
+ const commentCount = createMemo(() => {
298
+ if (store.mode === "shell") return 0
299
+ return prompt.context.items().filter((item) => !!item.comment?.trim()).length
300
+ })
301
+
302
+ const contextItems = createMemo(() => {
303
+ const items = prompt.context.items()
304
+ if (store.mode !== "shell") return items
305
+ return items.filter((item) => !item.comment?.trim())
306
+ })
307
+
308
+ const hasUserPrompt = createMemo(() => {
309
+ const sessionID = params.id
310
+ if (!sessionID) return false
311
+ const messages = sync.data.message[sessionID]
312
+ if (!messages) return false
313
+ return messages.some((m) => m.role === "user")
314
+ })
315
+
316
+ const [history, setHistory] = persisted(
317
+ Persist.global("prompt-history", ["prompt-history.v1"]),
318
+ createStore<{
319
+ entries: PromptHistoryStoredEntry[]
320
+ }>({
321
+ entries: [],
322
+ }),
323
+ )
324
+ const [shellHistory, setShellHistory] = persisted(
325
+ Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
326
+ createStore<{
327
+ entries: PromptHistoryStoredEntry[]
328
+ }>({
329
+ entries: [],
330
+ }),
331
+ )
332
+
333
+ const suggest = createMemo(() => !hasUserPrompt())
334
+
335
+ const placeholder = createMemo(() =>
336
+ promptPlaceholder({
337
+ mode: store.mode,
338
+ commentCount: commentCount(),
339
+ example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
340
+ suggest: suggest(),
341
+ t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
342
+ }),
343
+ )
344
+
345
+ const historyComments = () => {
346
+ const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
347
+ return prompt.context.items().flatMap((item) => {
348
+ if (item.type !== "file") return []
349
+ const comment = item.comment?.trim()
350
+ if (!comment) return []
351
+
352
+ const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined
353
+ const nextSelection =
354
+ selection ??
355
+ (item.selection
356
+ ? ({
357
+ start: item.selection.startLine,
358
+ end: item.selection.endLine,
359
+ } satisfies SelectedLineRange)
360
+ : undefined)
361
+ if (!nextSelection) return []
362
+
363
+ return [
364
+ {
365
+ id: item.commentID ?? item.key,
366
+ path: item.path,
367
+ selection: { ...nextSelection },
368
+ comment,
369
+ time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(),
370
+ origin: item.commentOrigin,
371
+ preview: item.preview,
372
+ } satisfies PromptHistoryComment,
373
+ ]
374
+ })
375
+ }
376
+
377
+ const applyHistoryComments = (items: PromptHistoryComment[]) => {
378
+ comments.replace(
379
+ items.map((item) => ({
380
+ id: item.id,
381
+ file: item.path,
382
+ selection: { ...item.selection },
383
+ comment: item.comment,
384
+ time: item.time,
385
+ })),
386
+ )
387
+ prompt.context.replaceComments(
388
+ items.map((item) => ({
389
+ type: "file" as const,
390
+ path: item.path,
391
+ selection: selectionFromLines(item.selection),
392
+ comment: item.comment,
393
+ commentID: item.id,
394
+ commentOrigin: item.origin,
395
+ preview: item.preview,
396
+ })),
397
+ )
398
+ }
399
+
400
+ const applyHistoryPrompt = (entry: PromptHistoryEntry, position: "start" | "end") => {
401
+ const p = entry.prompt
402
+ const length = position === "start" ? 0 : promptLength(p)
403
+ setStore("applyingHistory", true)
404
+ applyHistoryComments(entry.comments)
405
+ prompt.set(p, length)
406
+ requestAnimationFrame(() => {
407
+ editorRef.focus()
408
+ setCursorPosition(editorRef, length)
409
+ setStore("applyingHistory", false)
410
+ queueScroll()
411
+ })
412
+ }
413
+
414
+ const getCaretState = () => {
415
+ const selection = window.getSelection()
416
+ const textLength = promptLength(prompt.current())
417
+ if (!selection || selection.rangeCount === 0) {
418
+ return { collapsed: false, cursorPosition: 0, textLength }
419
+ }
420
+ const anchorNode = selection.anchorNode
421
+ if (!anchorNode || !editorRef.contains(anchorNode)) {
422
+ return { collapsed: false, cursorPosition: 0, textLength }
423
+ }
424
+ return {
425
+ collapsed: selection.isCollapsed,
426
+ cursorPosition: getCursorPosition(editorRef),
427
+ textLength,
428
+ }
429
+ }
430
+
431
+ const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
432
+
433
+ const pick = () => fileInputRef?.click()
434
+
435
+ const setMode = (mode: "normal" | "shell") => {
436
+ setStore("mode", mode)
437
+ setStore("popover", null)
438
+ requestAnimationFrame(() => editorRef?.focus())
439
+ }
440
+
441
+ const shellModeKey = "mod+shift+x"
442
+ const normalModeKey = "mod+shift+e"
443
+
444
+ command.register("prompt-input", () => [
445
+ {
446
+ id: "file.attach",
447
+ title: language.t("prompt.action.attachFile"),
448
+ category: language.t("command.category.file"),
449
+ keybind: "mod+u",
450
+ disabled: store.mode !== "normal",
451
+ onSelect: pick,
452
+ },
453
+ {
454
+ id: "prompt.mode.shell",
455
+ title: language.t("command.prompt.mode.shell"),
456
+ category: language.t("command.category.session"),
457
+ keybind: shellModeKey,
458
+ disabled: store.mode === "shell",
459
+ onSelect: () => setMode("shell"),
460
+ },
461
+ {
462
+ id: "prompt.mode.normal",
463
+ title: language.t("command.prompt.mode.normal"),
464
+ category: language.t("command.category.session"),
465
+ keybind: normalModeKey,
466
+ disabled: store.mode === "normal",
467
+ onSelect: () => setMode("normal"),
468
+ },
469
+ ])
470
+
471
+ const closePopover = () => setStore("popover", null)
472
+
473
+ const resetHistoryNavigation = (force = false) => {
474
+ if (!force && (store.historyIndex < 0 || store.applyingHistory)) return
475
+ setStore("historyIndex", -1)
476
+ setStore("savedPrompt", null)
477
+ }
478
+
479
+ const clearEditor = () => {
480
+ editorRef.innerHTML = ""
481
+ }
482
+
483
+ const setEditorText = (text: string) => {
484
+ clearEditor()
485
+ editorRef.textContent = text
486
+ }
487
+
488
+ const focusEditorEnd = () => {
489
+ requestAnimationFrame(() => {
490
+ editorRef.focus()
491
+ const range = document.createRange()
492
+ const selection = window.getSelection()
493
+ range.selectNodeContents(editorRef)
494
+ range.collapse(false)
495
+ selection?.removeAllRanges()
496
+ selection?.addRange(range)
497
+ })
498
+ }
499
+
500
+ const currentCursor = () => {
501
+ const selection = window.getSelection()
502
+ if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null
503
+ return getCursorPosition(editorRef)
504
+ }
505
+
506
+ const renderEditorWithCursor = (parts: Prompt) => {
507
+ const cursor = currentCursor()
508
+ renderEditor(parts)
509
+ if (cursor !== null) setCursorPosition(editorRef, cursor)
510
+ }
511
+
512
+ createEffect(() => {
513
+ params.id
514
+ if (params.id) return
515
+ if (!suggest()) return
516
+ const interval = setInterval(() => {
517
+ setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
518
+ }, 6500)
519
+ onCleanup(() => clearInterval(interval))
520
+ })
521
+
522
+ const [composing, setComposing] = createSignal(false)
523
+ const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
524
+
525
+ const handleBlur = () => {
526
+ closePopover()
527
+ setComposing(false)
528
+ }
529
+
530
+ const handleCompositionStart = () => {
531
+ setComposing(true)
532
+ }
533
+
534
+ const handleCompositionEnd = () => {
535
+ setComposing(false)
536
+ requestAnimationFrame(() => {
537
+ if (composing()) return
538
+ reconcile(prompt.current().filter((part) => part.type !== "image"))
539
+ })
540
+ }
541
+
542
+ const agentList = createMemo(() =>
543
+ sync.data.agent
544
+ .filter((agent) => !agent.hidden && agent.mode !== "primary")
545
+ .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
546
+ )
547
+ const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name))
548
+
549
+ const handleAtSelect = (option: AtOption | undefined) => {
550
+ if (!option) return
551
+ if (option.type === "agent") {
552
+ addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
553
+ } else {
554
+ addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
555
+ }
556
+ }
557
+
558
+ const atKey = (x: AtOption | undefined) => {
559
+ if (!x) return ""
560
+ return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}`
561
+ }
562
+
563
+ const {
564
+ flat: atFlat,
565
+ active: atActive,
566
+ setActive: setAtActive,
567
+ onInput: atOnInput,
568
+ onKeyDown: atOnKeyDown,
569
+ } = useFilteredList<AtOption>({
570
+ items: async (query) => {
571
+ const agents = agentList()
572
+ const open = recent()
573
+ const seen = new Set(open)
574
+ const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
575
+ const paths = await files.searchFilesAndDirectories(query)
576
+ const fileOptions: AtOption[] = paths
577
+ .filter((path) => !seen.has(path))
578
+ .map((path) => ({ type: "file", path, display: path }))
579
+ return [...agents, ...pinned, ...fileOptions]
580
+ },
581
+ key: atKey,
582
+ filterKeys: ["display"],
583
+ groupBy: (item) => {
584
+ if (item.type === "agent") return "agent"
585
+ if (item.recent) return "recent"
586
+ return "file"
587
+ },
588
+ sortGroupsBy: (a, b) => {
589
+ const rank = (category: string) => {
590
+ if (category === "agent") return 0
591
+ if (category === "recent") return 1
592
+ return 2
593
+ }
594
+ return rank(a.category) - rank(b.category)
595
+ },
596
+ onSelect: handleAtSelect,
597
+ })
598
+
599
+ const slashCommands = createMemo<SlashCommand[]>(() => {
600
+ const builtin = command.options
601
+ .filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash)
602
+ .map((opt) => ({
603
+ id: opt.id,
604
+ trigger: opt.slash!,
605
+ title: opt.title,
606
+ description: opt.description,
607
+ keybind: opt.keybind,
608
+ type: "builtin" as const,
609
+ }))
610
+
611
+ const custom = sync.data.command.map((cmd) => ({
612
+ id: `custom.${cmd.name}`,
613
+ trigger: cmd.name,
614
+ title: cmd.name,
615
+ description: cmd.description,
616
+ type: "custom" as const,
617
+ source: cmd.source,
618
+ }))
619
+
620
+ return [...custom, ...builtin]
621
+ })
622
+
623
+ const handleSlashSelect = (cmd: SlashCommand | undefined) => {
624
+ if (!cmd) return
625
+ promptProbe.select(cmd.id)
626
+ closePopover()
627
+
628
+ if (cmd.type === "custom") {
629
+ const text = `/${cmd.trigger} `
630
+ setEditorText(text)
631
+ prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
632
+ focusEditorEnd()
633
+ return
634
+ }
635
+
636
+ clearEditor()
637
+ prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
638
+ command.trigger(cmd.id, "slash")
639
+ }
640
+
641
+ const {
642
+ flat: slashFlat,
643
+ active: slashActive,
644
+ setActive: setSlashActive,
645
+ onInput: slashOnInput,
646
+ onKeyDown: slashOnKeyDown,
647
+ } = useFilteredList<SlashCommand>({
648
+ items: slashCommands,
649
+ key: (x) => x?.id,
650
+ filterKeys: ["trigger", "title"],
651
+ onSelect: handleSlashSelect,
652
+ })
653
+
654
+ const createPill = (part: FileAttachmentPart | AgentPart) => {
655
+ const pill = document.createElement("span")
656
+ pill.textContent = part.content
657
+ pill.setAttribute("data-type", part.type)
658
+ if (part.type === "file") pill.setAttribute("data-path", part.path)
659
+ if (part.type === "agent") pill.setAttribute("data-name", part.name)
660
+ pill.setAttribute("contenteditable", "false")
661
+ pill.style.userSelect = "text"
662
+ pill.style.cursor = "default"
663
+ return pill
664
+ }
665
+
666
+ const isNormalizedEditor = () =>
667
+ Array.from(editorRef.childNodes).every((node) => {
668
+ if (node.nodeType === Node.TEXT_NODE) {
669
+ const text = node.textContent ?? ""
670
+ if (!text.includes("\u200B")) return true
671
+ if (text !== "\u200B") return false
672
+
673
+ const prev = node.previousSibling
674
+ const next = node.nextSibling
675
+ const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
676
+ return !!prevIsBr && !next
677
+ }
678
+ if (node.nodeType !== Node.ELEMENT_NODE) return false
679
+ const el = node as HTMLElement
680
+ if (el.dataset.type === "file") return true
681
+ if (el.dataset.type === "agent") return true
682
+ return el.tagName === "BR"
683
+ })
684
+
685
+ const renderEditor = (parts: Prompt) => {
686
+ clearEditor()
687
+ for (const part of parts) {
688
+ if (part.type === "text") {
689
+ editorRef.appendChild(createTextFragment(part.content))
690
+ continue
691
+ }
692
+ if (part.type === "file" || part.type === "agent") {
693
+ editorRef.appendChild(createPill(part))
694
+ }
695
+ }
696
+
697
+ const last = editorRef.lastChild
698
+ if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") {
699
+ editorRef.appendChild(document.createTextNode("\u200B"))
700
+ }
701
+ }
702
+
703
+ // Auto-scroll active command into view when navigating with keyboard
704
+ createEffect(() => {
705
+ const activeId = slashActive()
706
+ if (!activeId || !slashPopoverRef) return
707
+
708
+ requestAnimationFrame(() => {
709
+ const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
710
+ element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
711
+ })
712
+ })
713
+
714
+ if (promptEnabled()) {
715
+ createEffect(() => {
716
+ promptProbe.set({
717
+ popover: store.popover,
718
+ slash: {
719
+ active: slashActive() ?? null,
720
+ ids: slashFlat().map((cmd) => cmd.id),
721
+ },
722
+ })
723
+ })
724
+
725
+ onCleanup(() => promptProbe.clear())
726
+ }
727
+
728
+ const selectPopoverActive = () => {
729
+ if (store.popover === "at") {
730
+ const items = atFlat()
731
+ if (items.length === 0) return
732
+ const active = atActive()
733
+ const item = items.find((entry) => atKey(entry) === active) ?? items[0]
734
+ handleAtSelect(item)
735
+ return
736
+ }
737
+
738
+ if (store.popover === "slash") {
739
+ const items = slashFlat()
740
+ if (items.length === 0) return
741
+ const active = slashActive()
742
+ const item = items.find((entry) => entry.id === active) ?? items[0]
743
+ handleSlashSelect(item)
744
+ }
745
+ }
746
+
747
+ const reconcile = (input: Prompt) => {
748
+ if (mirror.input) {
749
+ mirror.input = false
750
+ if (isNormalizedEditor()) return
751
+
752
+ renderEditorWithCursor(input)
753
+ return
754
+ }
755
+
756
+ const dom = parseFromDOM()
757
+ if (isNormalizedEditor() && isPromptEqual(input, dom)) return
758
+
759
+ renderEditorWithCursor(input)
760
+ }
761
+
762
+ createEffect(
763
+ on(
764
+ () => prompt.current(),
765
+ (parts) => {
766
+ if (composing()) return
767
+ reconcile(parts.filter((part) => part.type !== "image"))
768
+ },
769
+ ),
770
+ )
771
+
772
+ const parseFromDOM = (): Prompt => {
773
+ const parts: Prompt = []
774
+ let position = 0
775
+ let buffer = ""
776
+
777
+ const flushText = () => {
778
+ let content = buffer
779
+ if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n")
780
+ if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
781
+ buffer = ""
782
+ if (!content) return
783
+ parts.push({ type: "text", content, start: position, end: position + content.length })
784
+ position += content.length
785
+ }
786
+
787
+ const pushFile = (file: HTMLElement) => {
788
+ const content = file.textContent ?? ""
789
+ parts.push({
790
+ type: "file",
791
+ path: file.dataset.path!,
792
+ content,
793
+ start: position,
794
+ end: position + content.length,
795
+ })
796
+ position += content.length
797
+ }
798
+
799
+ const pushAgent = (agent: HTMLElement) => {
800
+ const content = agent.textContent ?? ""
801
+ parts.push({
802
+ type: "agent",
803
+ name: agent.dataset.name!,
804
+ content,
805
+ start: position,
806
+ end: position + content.length,
807
+ })
808
+ position += content.length
809
+ }
810
+
811
+ const visit = (node: Node) => {
812
+ if (node.nodeType === Node.TEXT_NODE) {
813
+ buffer += node.textContent ?? ""
814
+ return
815
+ }
816
+ if (node.nodeType !== Node.ELEMENT_NODE) return
817
+
818
+ const el = node as HTMLElement
819
+ if (el.dataset.type === "file") {
820
+ flushText()
821
+ pushFile(el)
822
+ return
823
+ }
824
+ if (el.dataset.type === "agent") {
825
+ flushText()
826
+ pushAgent(el)
827
+ return
828
+ }
829
+ if (el.tagName === "BR") {
830
+ buffer += "\n"
831
+ return
832
+ }
833
+
834
+ for (const child of Array.from(el.childNodes)) {
835
+ visit(child)
836
+ }
837
+ }
838
+
839
+ const children = Array.from(editorRef.childNodes)
840
+ children.forEach((child, index) => {
841
+ const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName)
842
+ visit(child)
843
+ if (isBlock && index < children.length - 1) {
844
+ buffer += "\n"
845
+ }
846
+ })
847
+
848
+ flushText()
849
+
850
+ if (parts.length === 0) parts.push(...DEFAULT_PROMPT)
851
+ return parts
852
+ }
853
+
854
+ const handleInput = () => {
855
+ const rawParts = parseFromDOM()
856
+ const images = imageAttachments()
857
+ const cursorPosition = getCursorPosition(editorRef)
858
+ const rawText =
859
+ rawParts.length === 1 && rawParts[0]?.type === "text"
860
+ ? rawParts[0].content
861
+ : rawParts.map((p) => ("content" in p ? p.content : "")).join("")
862
+ const hasNonText = rawParts.some((part) => part.type !== "text")
863
+ const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0
864
+
865
+ if (shouldReset) {
866
+ closePopover()
867
+ resetHistoryNavigation()
868
+ if (prompt.dirty()) {
869
+ mirror.input = true
870
+ prompt.set(DEFAULT_PROMPT, 0)
871
+ }
872
+ queueScroll()
873
+ return
874
+ }
875
+
876
+ const shellMode = store.mode === "shell"
877
+
878
+ if (!shellMode) {
879
+ const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/)
880
+ const slashMatch = rawText.match(/^\/(\S*)$/)
881
+
882
+ if (atMatch) {
883
+ atOnInput(atMatch[1])
884
+ setStore("popover", "at")
885
+ } else if (slashMatch) {
886
+ slashOnInput(slashMatch[1])
887
+ setStore("popover", "slash")
888
+ } else {
889
+ closePopover()
890
+ }
891
+ } else {
892
+ closePopover()
893
+ }
894
+
895
+ resetHistoryNavigation()
896
+
897
+ mirror.input = true
898
+ prompt.set([...rawParts, ...images], cursorPosition)
899
+ queueScroll()
900
+ }
901
+
902
+ const addPart = (part: ContentPart) => {
903
+ if (part.type === "image") return false
904
+
905
+ const selection = window.getSelection()
906
+ if (!selection) return false
907
+
908
+ if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) {
909
+ editorRef.focus()
910
+ const cursor = prompt.cursor() ?? promptLength(prompt.current())
911
+ setCursorPosition(editorRef, cursor)
912
+ }
913
+
914
+ if (selection.rangeCount === 0) return false
915
+ const range = selection.getRangeAt(0)
916
+ if (!editorRef.contains(range.startContainer)) return false
917
+
918
+ if (part.type === "file" || part.type === "agent") {
919
+ const cursorPosition = getCursorPosition(editorRef)
920
+ const rawText = prompt
921
+ .current()
922
+ .map((p) => ("content" in p ? p.content : ""))
923
+ .join("")
924
+ const textBeforeCursor = rawText.substring(0, cursorPosition)
925
+ const atMatch = textBeforeCursor.match(/@(\S*)$/)
926
+ const pill = createPill(part)
927
+ const gap = document.createTextNode(" ")
928
+
929
+ if (atMatch) {
930
+ const start = atMatch.index ?? cursorPosition - atMatch[0].length
931
+ setRangeEdge(editorRef, range, "start", start)
932
+ setRangeEdge(editorRef, range, "end", cursorPosition)
933
+ }
934
+
935
+ range.deleteContents()
936
+ range.insertNode(gap)
937
+ range.insertNode(pill)
938
+ range.setStartAfter(gap)
939
+ range.collapse(true)
940
+ selection.removeAllRanges()
941
+ selection.addRange(range)
942
+ }
943
+
944
+ if (part.type === "text") {
945
+ const fragment = createTextFragment(part.content)
946
+ const last = fragment.lastChild
947
+ range.deleteContents()
948
+ range.insertNode(fragment)
949
+ if (last) {
950
+ if (last.nodeType === Node.TEXT_NODE) {
951
+ const text = last.textContent ?? ""
952
+ if (text === "\u200B") {
953
+ range.setStart(last, 0)
954
+ }
955
+ if (text !== "\u200B") {
956
+ range.setStart(last, text.length)
957
+ }
958
+ }
959
+ if (last.nodeType !== Node.TEXT_NODE) {
960
+ const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR"
961
+ const next = last.nextSibling
962
+ const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === ""
963
+ if (isBreak && (!next || emptyText)) {
964
+ const placeholder = next && emptyText ? next : document.createTextNode("\u200B")
965
+ if (!next) last.parentNode?.insertBefore(placeholder, null)
966
+ placeholder.textContent = "\u200B"
967
+ range.setStart(placeholder, 0)
968
+ } else {
969
+ range.setStartAfter(last)
970
+ }
971
+ }
972
+ }
973
+ range.collapse(true)
974
+ selection.removeAllRanges()
975
+ selection.addRange(range)
976
+ }
977
+
978
+ handleInput()
979
+ closePopover()
980
+ return true
981
+ }
982
+
983
+ const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
984
+ const currentHistory = mode === "shell" ? shellHistory : history
985
+ const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
986
+ const next = prependHistoryEntry(currentHistory.entries, prompt, mode === "shell" ? [] : historyComments())
987
+ if (next === currentHistory.entries) return
988
+ setCurrentHistory("entries", next)
989
+ }
990
+
991
+ createEffect(
992
+ on(
993
+ () => props.edit?.id,
994
+ (id) => {
995
+ const edit = props.edit
996
+ if (!id || !edit) return
997
+
998
+ for (const item of prompt.context.items()) {
999
+ prompt.context.remove(item.key)
1000
+ }
1001
+
1002
+ for (const item of edit.context) {
1003
+ prompt.context.add({
1004
+ type: item.type,
1005
+ path: item.path,
1006
+ selection: item.selection,
1007
+ comment: item.comment,
1008
+ commentID: item.commentID,
1009
+ commentOrigin: item.commentOrigin,
1010
+ preview: item.preview,
1011
+ })
1012
+ }
1013
+
1014
+ setStore("mode", "normal")
1015
+ setStore("popover", null)
1016
+ setStore("historyIndex", -1)
1017
+ setStore("savedPrompt", null)
1018
+ prompt.set(edit.prompt, promptLength(edit.prompt))
1019
+ requestAnimationFrame(() => {
1020
+ editorRef.focus()
1021
+ setCursorPosition(editorRef, promptLength(edit.prompt))
1022
+ queueScroll()
1023
+ })
1024
+ props.onEditLoaded?.()
1025
+ },
1026
+ { defer: true },
1027
+ ),
1028
+ )
1029
+
1030
+ const navigateHistory = (direction: "up" | "down") => {
1031
+ const result = navigatePromptHistory({
1032
+ direction,
1033
+ entries: store.mode === "shell" ? shellHistory.entries : history.entries,
1034
+ historyIndex: store.historyIndex,
1035
+ currentPrompt: prompt.current(),
1036
+ currentComments: historyComments(),
1037
+ savedPrompt: store.savedPrompt,
1038
+ })
1039
+ if (!result.handled) return false
1040
+ setStore("historyIndex", result.historyIndex)
1041
+ setStore("savedPrompt", result.savedPrompt)
1042
+ applyHistoryPrompt(result.entry, result.cursor)
1043
+ return true
1044
+ }
1045
+
1046
+ const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({
1047
+ editor: () => editorRef,
1048
+ isDialogActive: () => !!dialog.active,
1049
+ setDraggingType: (type) => setStore("draggingType", type),
1050
+ focusEditor: () => {
1051
+ editorRef.focus()
1052
+ setCursorPosition(editorRef, promptLength(prompt.current()))
1053
+ },
1054
+ addPart,
1055
+ readClipboardImage: platform.readClipboardImage,
1056
+ })
1057
+
1058
+ const variants = createMemo(() => ["default", ...local.model.variant.list()])
1059
+ const accepting = createMemo(() => {
1060
+ const id = params.id
1061
+ if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
1062
+ return permission.isAutoAccepting(id, sdk.directory)
1063
+ })
1064
+ const acceptLabel = createMemo(() =>
1065
+ language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
1066
+ )
1067
+ const toggleAccept = () => {
1068
+ if (!params.id) {
1069
+ permission.toggleAutoAcceptDirectory(sdk.directory)
1070
+ return
1071
+ }
1072
+
1073
+ permission.toggleAutoAccept(params.id, sdk.directory)
1074
+ }
1075
+
1076
+ const { abort, handleSubmit } = createPromptSubmit({
1077
+ info,
1078
+ imageAttachments,
1079
+ commentCount,
1080
+ autoAccept: () => accepting(),
1081
+ mode: () => store.mode,
1082
+ working,
1083
+ editor: () => editorRef,
1084
+ queueScroll,
1085
+ promptLength,
1086
+ addToHistory,
1087
+ resetHistoryNavigation: () => {
1088
+ resetHistoryNavigation(true)
1089
+ },
1090
+ setMode: (mode) => setStore("mode", mode),
1091
+ setPopover: (popover) => setStore("popover", popover),
1092
+ newSessionWorktree: () => props.newSessionWorktree,
1093
+ onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
1094
+ shouldQueue: props.shouldQueue,
1095
+ onQueue: props.onQueue,
1096
+ onAbort: props.onAbort,
1097
+ onSubmit: props.onSubmit,
1098
+ })
1099
+
1100
+ const handleKeyDown = (event: KeyboardEvent) => {
1101
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
1102
+ event.preventDefault()
1103
+ if (store.mode !== "normal") return
1104
+ pick()
1105
+ return
1106
+ }
1107
+
1108
+ if (event.key === "Backspace") {
1109
+ const selection = window.getSelection()
1110
+ if (selection && selection.isCollapsed) {
1111
+ const node = selection.anchorNode
1112
+ const offset = selection.anchorOffset
1113
+ if (node && node.nodeType === Node.TEXT_NODE) {
1114
+ const text = node.textContent ?? ""
1115
+ if (/^\u200B+$/.test(text) && offset > 0) {
1116
+ const range = document.createRange()
1117
+ range.setStart(node, 0)
1118
+ range.collapse(true)
1119
+ selection.removeAllRanges()
1120
+ selection.addRange(range)
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+
1126
+ if (event.key === "!" && store.mode === "normal") {
1127
+ const cursorPosition = getCursorPosition(editorRef)
1128
+ if (cursorPosition === 0) {
1129
+ setStore("mode", "shell")
1130
+ setStore("popover", null)
1131
+ event.preventDefault()
1132
+ return
1133
+ }
1134
+ }
1135
+
1136
+ if (event.key === "Escape") {
1137
+ if (store.popover) {
1138
+ closePopover()
1139
+ event.preventDefault()
1140
+ event.stopPropagation()
1141
+ return
1142
+ }
1143
+
1144
+ if (store.mode === "shell") {
1145
+ setStore("mode", "normal")
1146
+ event.preventDefault()
1147
+ event.stopPropagation()
1148
+ return
1149
+ }
1150
+
1151
+ if (working()) {
1152
+ abort()
1153
+ event.preventDefault()
1154
+ event.stopPropagation()
1155
+ return
1156
+ }
1157
+
1158
+ if (escBlur()) {
1159
+ editorRef.blur()
1160
+ event.preventDefault()
1161
+ event.stopPropagation()
1162
+ return
1163
+ }
1164
+ }
1165
+
1166
+ if (store.mode === "shell") {
1167
+ const { collapsed, cursorPosition, textLength } = getCaretState()
1168
+ if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) {
1169
+ setStore("mode", "normal")
1170
+ event.preventDefault()
1171
+ return
1172
+ }
1173
+ }
1174
+
1175
+ // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input
1176
+ // and should always insert a newline regardless of composition state
1177
+ if (event.key === "Enter" && event.shiftKey) {
1178
+ addPart({ type: "text", content: "\n", start: 0, end: 0 })
1179
+ event.preventDefault()
1180
+ return
1181
+ }
1182
+
1183
+ if (event.key === "Enter" && isImeComposing(event)) {
1184
+ return
1185
+ }
1186
+
1187
+ const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
1188
+
1189
+ if (store.popover) {
1190
+ if (event.key === "Tab") {
1191
+ selectPopoverActive()
1192
+ event.preventDefault()
1193
+ return
1194
+ }
1195
+ const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter"
1196
+ const ctrlNav = ctrl && (event.key === "n" || event.key === "p")
1197
+ if (nav || ctrlNav) {
1198
+ if (store.popover === "at") {
1199
+ atOnKeyDown(event)
1200
+ event.preventDefault()
1201
+ return
1202
+ }
1203
+ if (store.popover === "slash") {
1204
+ slashOnKeyDown(event)
1205
+ }
1206
+ event.preventDefault()
1207
+ return
1208
+ }
1209
+ }
1210
+
1211
+ if (ctrl && event.code === "KeyG") {
1212
+ if (store.popover) {
1213
+ closePopover()
1214
+ event.preventDefault()
1215
+ return
1216
+ }
1217
+ if (working()) {
1218
+ abort()
1219
+ event.preventDefault()
1220
+ }
1221
+ return
1222
+ }
1223
+
1224
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
1225
+ if (event.altKey || event.ctrlKey || event.metaKey) return
1226
+ const { collapsed } = getCaretState()
1227
+ if (!collapsed) return
1228
+
1229
+ const cursorPosition = getCursorPosition(editorRef)
1230
+ const textContent = prompt
1231
+ .current()
1232
+ .map((part) => ("content" in part ? part.content : ""))
1233
+ .join("")
1234
+ const direction = event.key === "ArrowUp" ? "up" : "down"
1235
+ if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return
1236
+ if (navigateHistory(direction)) {
1237
+ event.preventDefault()
1238
+ }
1239
+ return
1240
+ }
1241
+
1242
+ // Note: Shift+Enter is handled earlier, before IME check
1243
+ if (event.key === "Enter" && !event.shiftKey) {
1244
+ event.preventDefault()
1245
+ if (event.repeat) return
1246
+ if (
1247
+ working() &&
1248
+ prompt
1249
+ .current()
1250
+ .map((part) => ("content" in part ? part.content : ""))
1251
+ .join("")
1252
+ .trim().length === 0 &&
1253
+ imageAttachments().length === 0 &&
1254
+ commentCount() === 0
1255
+ ) {
1256
+ return
1257
+ }
1258
+ handleSubmit(event)
1259
+ }
1260
+ }
1261
+
1262
+ return (
1263
+ <div class="relative size-full _max-h-[320px] flex flex-col gap-0">
1264
+ <PromptPopover
1265
+ popover={store.popover}
1266
+ setSlashPopoverRef={(el) => (slashPopoverRef = el)}
1267
+ atFlat={atFlat()}
1268
+ atActive={atActive() ?? undefined}
1269
+ atKey={atKey}
1270
+ setAtActive={setAtActive}
1271
+ onAtSelect={handleAtSelect}
1272
+ slashFlat={slashFlat()}
1273
+ slashActive={slashActive() ?? undefined}
1274
+ setSlashActive={setSlashActive}
1275
+ onSlashSelect={handleSlashSelect}
1276
+ commandKeybind={command.keybind}
1277
+ t={(key) => language.t(key as Parameters<typeof language.t>[0])}
1278
+ />
1279
+ <DockShellForm
1280
+ onSubmit={handleSubmit}
1281
+ classList={{
1282
+ "group/prompt-input": true,
1283
+ "focus-within:shadow-xs-border": true,
1284
+ "border-icon-info-active border-dashed": store.draggingType !== null,
1285
+ [props.class ?? ""]: !!props.class,
1286
+ }}
1287
+ >
1288
+ <PromptDragOverlay
1289
+ type={store.draggingType}
1290
+ label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")}
1291
+ />
1292
+ <PromptContextItems
1293
+ items={contextItems()}
1294
+ active={(item) => {
1295
+ const active = comments.active()
1296
+ return !!item.commentID && item.commentID === active?.id && item.path === active?.file
1297
+ }}
1298
+ openComment={openComment}
1299
+ remove={(item) => {
1300
+ if (item.commentID) comments.remove(item.path, item.commentID)
1301
+ prompt.context.remove(item.key)
1302
+ }}
1303
+ t={(key) => language.t(key as Parameters<typeof language.t>[0])}
1304
+ />
1305
+ <PromptImageAttachments
1306
+ attachments={imageAttachments()}
1307
+ onOpen={(attachment) =>
1308
+ dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
1309
+ }
1310
+ onRemove={removeAttachment}
1311
+ removeLabel={language.t("prompt.attachment.remove")}
1312
+ />
1313
+ <div
1314
+ class="relative"
1315
+ onMouseDown={(e) => {
1316
+ const target = e.target
1317
+ if (!(target instanceof HTMLElement)) return
1318
+ if (
1319
+ target.closest(
1320
+ '[data-action="prompt-attach"], [data-action="prompt-submit"], [data-action="prompt-permissions"]',
1321
+ )
1322
+ ) {
1323
+ return
1324
+ }
1325
+ editorRef?.focus()
1326
+ }}
1327
+ >
1328
+ <div
1329
+ class="relative max-h-[240px] overflow-y-auto no-scrollbar"
1330
+ ref={(el) => (scrollRef = el)}
1331
+ style={{ "scroll-padding-bottom": space }}
1332
+ >
1333
+ <div
1334
+ data-component="prompt-input"
1335
+ ref={(el) => {
1336
+ editorRef = el
1337
+ props.ref?.(el)
1338
+ }}
1339
+ role="textbox"
1340
+ aria-multiline="true"
1341
+ aria-label={placeholder()}
1342
+ contenteditable="true"
1343
+ autocapitalize={store.mode === "normal" ? "sentences" : "off"}
1344
+ autocorrect={store.mode === "normal" ? "on" : "off"}
1345
+ spellcheck={store.mode === "normal"}
1346
+ onInput={handleInput}
1347
+ onPaste={handlePaste}
1348
+ onCompositionStart={handleCompositionStart}
1349
+ onCompositionEnd={handleCompositionEnd}
1350
+ onBlur={handleBlur}
1351
+ onKeyDown={handleKeyDown}
1352
+ classList={{
1353
+ "select-text": true,
1354
+ "w-full pl-3 pr-2 pt-2 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
1355
+ "[&_[data-type=file]]:text-syntax-property": true,
1356
+ "[&_[data-type=agent]]:text-syntax-type": true,
1357
+ "font-mono!": store.mode === "shell",
1358
+ }}
1359
+ style={{ "padding-bottom": space }}
1360
+ />
1361
+ <Show when={!prompt.dirty()}>
1362
+ <div
1363
+ class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
1364
+ classList={{ "font-mono!": store.mode === "shell" }}
1365
+ style={{ "padding-bottom": space }}
1366
+ >
1367
+ {placeholder()}
1368
+ </div>
1369
+ </Show>
1370
+ </div>
1371
+
1372
+ <div
1373
+ aria-hidden="true"
1374
+ class="pointer-events-none absolute inset-x-0 bottom-0"
1375
+ style={{
1376
+ height: space,
1377
+ background:
1378
+ "linear-gradient(to top, var(--surface-raised-stronger-non-alpha) calc(100% - 20px), transparent)",
1379
+ }}
1380
+ />
1381
+
1382
+ <div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
1383
+ <input
1384
+ ref={fileInputRef}
1385
+ type="file"
1386
+ multiple
1387
+ accept={ACCEPTED_FILE_TYPES.join(",")}
1388
+ class="hidden"
1389
+ onChange={(e) => {
1390
+ const list = e.currentTarget.files
1391
+ if (list) void addAttachments(Array.from(list))
1392
+ e.currentTarget.value = ""
1393
+ }}
1394
+ />
1395
+
1396
+ <div class="flex items-center gap-1 pointer-events-auto">
1397
+ <Tooltip placement="top" inactive={!prompt.dirty() && !working()} value={tip()}>
1398
+ <IconButton
1399
+ data-action="prompt-submit"
1400
+ type="submit"
1401
+ disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
1402
+ tabIndex={store.mode === "normal" ? undefined : -1}
1403
+ icon={working() ? "stop" : "arrow-up"}
1404
+ variant="primary"
1405
+ class="size-8"
1406
+ style={buttons()}
1407
+ aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
1408
+ />
1409
+ </Tooltip>
1410
+ </div>
1411
+ </div>
1412
+
1413
+ <div class="pointer-events-none absolute bottom-2 left-2">
1414
+ <div
1415
+ aria-hidden={store.mode !== "normal"}
1416
+ class="pointer-events-auto"
1417
+ style={{
1418
+ "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
1419
+ }}
1420
+ >
1421
+ <TooltipKeybind
1422
+ placement="top"
1423
+ title={language.t("prompt.action.attachFile")}
1424
+ keybind={command.keybind("file.attach")}
1425
+ >
1426
+ <Button
1427
+ data-action="prompt-attach"
1428
+ type="button"
1429
+ variant="ghost"
1430
+ class="size-8 p-0"
1431
+ style={buttons()}
1432
+ onClick={pick}
1433
+ disabled={store.mode !== "normal"}
1434
+ tabIndex={store.mode === "normal" ? undefined : -1}
1435
+ aria-label={language.t("prompt.action.attachFile")}
1436
+ >
1437
+ <Icon name="plus" class="size-4.5" />
1438
+ </Button>
1439
+ </TooltipKeybind>
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+ </DockShellForm>
1444
+ <Show when={store.mode === "normal" || store.mode === "shell"}>
1445
+ <DockTray attach="top">
1446
+ <div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
1447
+ <div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
1448
+ <div
1449
+ class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
1450
+ style={{
1451
+ padding: "0 4px 0 8px",
1452
+ ...shell(),
1453
+ }}
1454
+ >
1455
+ <span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
1456
+ <div class="size-4 shrink-0" />
1457
+ </div>
1458
+ <div class="flex items-center gap-1.5 min-w-0 flex-1">
1459
+ <div data-component="prompt-agent-control">
1460
+ <TooltipKeybind
1461
+ placement="top"
1462
+ gutter={4}
1463
+ title={language.t("command.agent.cycle")}
1464
+ keybind={command.keybind("agent.cycle")}
1465
+ >
1466
+ <Select
1467
+ size="normal"
1468
+ options={agentNames()}
1469
+ current={local.agent.current()?.name ?? ""}
1470
+ onSelect={local.agent.set}
1471
+ class="capitalize max-w-[160px] text-text-base"
1472
+ valueClass="truncate text-13-regular text-text-base"
1473
+ triggerStyle={control()}
1474
+ triggerProps={{ "data-action": "prompt-agent" }}
1475
+ variant="ghost"
1476
+ />
1477
+ </TooltipKeybind>
1478
+ </div>
1479
+ <div data-component="prompt-model-control">
1480
+ <Show
1481
+ when={providers.paid().length > 0}
1482
+ fallback={
1483
+ <TooltipKeybind
1484
+ placement="top"
1485
+ gutter={4}
1486
+ title={language.t("command.model.choose")}
1487
+ keybind={command.keybind("model.choose")}
1488
+ >
1489
+ <Button
1490
+ data-action="prompt-model"
1491
+ as="div"
1492
+ variant="ghost"
1493
+ size="normal"
1494
+ class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
1495
+ style={control()}
1496
+ onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
1497
+ >
1498
+ <Show when={local.model.current()?.provider?.id}>
1499
+ <ProviderIcon
1500
+ id={local.model.current()?.provider?.id ?? ""}
1501
+ class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
1502
+ style={{ "will-change": "opacity", transform: "translateZ(0)" }}
1503
+ />
1504
+ </Show>
1505
+ <span class="truncate">
1506
+ {local.model.current()?.name ?? language.t("dialog.model.select.title")}
1507
+ </span>
1508
+ <Icon name="chevron-down" size="small" class="shrink-0" />
1509
+ </Button>
1510
+ </TooltipKeybind>
1511
+ }
1512
+ >
1513
+ <TooltipKeybind
1514
+ placement="top"
1515
+ gutter={4}
1516
+ title={language.t("command.model.choose")}
1517
+ keybind={command.keybind("model.choose")}
1518
+ >
1519
+ <ModelSelectorPopover
1520
+ model={local.model}
1521
+ triggerAs={Button}
1522
+ triggerProps={{
1523
+ variant: "ghost",
1524
+ size: "normal",
1525
+ style: control(),
1526
+ class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
1527
+ "data-action": "prompt-model",
1528
+ }}
1529
+ >
1530
+ <Show when={local.model.current()?.provider?.id}>
1531
+ <ProviderIcon
1532
+ id={local.model.current()?.provider?.id ?? ""}
1533
+ class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
1534
+ style={{ "will-change": "opacity", transform: "translateZ(0)" }}
1535
+ />
1536
+ </Show>
1537
+ <span class="truncate">
1538
+ {local.model.current()?.name ?? language.t("dialog.model.select.title")}
1539
+ </span>
1540
+ <Icon name="chevron-down" size="small" class="shrink-0" />
1541
+ </ModelSelectorPopover>
1542
+ </TooltipKeybind>
1543
+ </Show>
1544
+ </div>
1545
+ <div data-component="prompt-variant-control">
1546
+ <TooltipKeybind
1547
+ placement="top"
1548
+ gutter={4}
1549
+ title={language.t("command.model.variant.cycle")}
1550
+ keybind={command.keybind("model.variant.cycle")}
1551
+ >
1552
+ <Select
1553
+ size="normal"
1554
+ options={variants()}
1555
+ current={local.model.variant.current() ?? "default"}
1556
+ label={(x) => (x === "default" ? language.t("common.default") : x)}
1557
+ onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
1558
+ class="capitalize max-w-[160px] text-text-base"
1559
+ valueClass="truncate text-13-regular text-text-base"
1560
+ triggerStyle={control()}
1561
+ triggerProps={{ "data-action": "prompt-model-variant" }}
1562
+ variant="ghost"
1563
+ />
1564
+ </TooltipKeybind>
1565
+ </div>
1566
+ <TooltipKeybind
1567
+ placement="top"
1568
+ gutter={8}
1569
+ title={acceptLabel()}
1570
+ keybind={command.keybind("permissions.autoaccept")}
1571
+ >
1572
+ <Button
1573
+ data-action="prompt-permissions"
1574
+ variant="ghost"
1575
+ onClick={toggleAccept}
1576
+ classList={{
1577
+ "h-7 w-7 p-0 shrink-0 flex items-center justify-center": true,
1578
+ "text-text-base": !accepting(),
1579
+ "hover:bg-surface-success-base": accepting(),
1580
+ }}
1581
+ style={control()}
1582
+ aria-label={acceptLabel()}
1583
+ aria-pressed={accepting()}
1584
+ >
1585
+ <Icon name="shield" size="small" classList={{ "text-icon-success-base": accepting() }} />
1586
+ </Button>
1587
+ </TooltipKeybind>
1588
+ </div>
1589
+ </div>
1590
+ </div>
1591
+ </DockTray>
1592
+ </Show>
1593
+ </div>
1594
+ )
1595
+ }