@neurocode-ai/tui 1.18.8

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 (241) hide show
  1. package/bunfig.toml +4 -0
  2. package/package.json +72 -0
  3. package/src/app.tsx +1134 -0
  4. package/src/attention.ts +260 -0
  5. package/src/audio.d.ts +9 -0
  6. package/src/audio.ts +53 -0
  7. package/src/clipboard.ts +124 -0
  8. package/src/component/bg-pulse-render.ts +436 -0
  9. package/src/component/bg-pulse.tsx +99 -0
  10. package/src/component/command-palette.tsx +79 -0
  11. package/src/component/dialog-agent.tsx +31 -0
  12. package/src/component/dialog-console-org.tsx +135 -0
  13. package/src/component/dialog-debug.tsx +90 -0
  14. package/src/component/dialog-mcp.tsx +85 -0
  15. package/src/component/dialog-model.tsx +197 -0
  16. package/src/component/dialog-move-session.tsx +353 -0
  17. package/src/component/dialog-provider.tsx +469 -0
  18. package/src/component/dialog-retry-action.tsx +160 -0
  19. package/src/component/dialog-session-delete-failed.tsx +99 -0
  20. package/src/component/dialog-session-list.tsx +364 -0
  21. package/src/component/dialog-session-rename.tsx +31 -0
  22. package/src/component/dialog-skill.tsx +70 -0
  23. package/src/component/dialog-stash.tsx +87 -0
  24. package/src/component/dialog-status.tsx +168 -0
  25. package/src/component/dialog-tag.tsx +47 -0
  26. package/src/component/dialog-theme-list.tsx +50 -0
  27. package/src/component/dialog-variant.tsx +39 -0
  28. package/src/component/dialog-workspace-create.tsx +308 -0
  29. package/src/component/dialog-workspace-file-changes.tsx +144 -0
  30. package/src/component/dialog-workspace-list.tsx +112 -0
  31. package/src/component/dialog-workspace-unavailable.tsx +69 -0
  32. package/src/component/error-component.tsx +240 -0
  33. package/src/component/logo.tsx +61 -0
  34. package/src/component/plugin-route-missing.tsx +14 -0
  35. package/src/component/prompt/autocomplete.tsx +781 -0
  36. package/src/component/prompt/cwd.ts +0 -0
  37. package/src/component/prompt/frecency.tsx +1 -0
  38. package/src/component/prompt/history.tsx +1 -0
  39. package/src/component/prompt/index.tsx +1713 -0
  40. package/src/component/prompt/local-attachment.ts +48 -0
  41. package/src/component/prompt/move.tsx +205 -0
  42. package/src/component/prompt/stash.tsx +1 -0
  43. package/src/component/prompt/workspace.tsx +137 -0
  44. package/src/component/register-spinner.ts +6 -0
  45. package/src/component/spinner.tsx +26 -0
  46. package/src/component/startup-loading.tsx +63 -0
  47. package/src/component/todo-item.tsx +32 -0
  48. package/src/component/use-connected.tsx +12 -0
  49. package/src/component/workspace-label.tsx +19 -0
  50. package/src/config/index.tsx +129 -0
  51. package/src/config/keybind.ts +471 -0
  52. package/src/context/args.tsx +16 -0
  53. package/src/context/clipboard.tsx +18 -0
  54. package/src/context/data.tsx +569 -0
  55. package/src/context/directory.ts +17 -0
  56. package/src/context/editor.ts +408 -0
  57. package/src/context/epilogue.tsx +6 -0
  58. package/src/context/event.ts +36 -0
  59. package/src/context/exit.tsx +8 -0
  60. package/src/context/helper.tsx +26 -0
  61. package/src/context/kv.tsx +66 -0
  62. package/src/context/local.tsx +542 -0
  63. package/src/context/location.tsx +14 -0
  64. package/src/context/path-format.tsx +24 -0
  65. package/src/context/permission.tsx +26 -0
  66. package/src/context/project.tsx +115 -0
  67. package/src/context/prompt.tsx +18 -0
  68. package/src/context/route.tsx +60 -0
  69. package/src/context/runtime.tsx +62 -0
  70. package/src/context/sdk.tsx +151 -0
  71. package/src/context/sync.tsx +666 -0
  72. package/src/context/theme.tsx +332 -0
  73. package/src/context/thinking.ts +67 -0
  74. package/src/editor-zed.ts +286 -0
  75. package/src/editor.ts +101 -0
  76. package/src/feature-plugins/builtins.ts +36 -0
  77. package/src/feature-plugins/home/footer.tsx +100 -0
  78. package/src/feature-plugins/home/tips-view.tsx +287 -0
  79. package/src/feature-plugins/home/tips.tsx +59 -0
  80. package/src/feature-plugins/sidebar/context.tsx +65 -0
  81. package/src/feature-plugins/sidebar/files.tsx +70 -0
  82. package/src/feature-plugins/sidebar/footer.tsx +98 -0
  83. package/src/feature-plugins/sidebar/lsp.tsx +65 -0
  84. package/src/feature-plugins/sidebar/mcp.tsx +97 -0
  85. package/src/feature-plugins/sidebar/todo.tsx +49 -0
  86. package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  87. package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  88. package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  89. package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
  90. package/src/feature-plugins/system/notifications.ts +94 -0
  91. package/src/feature-plugins/system/plugins.tsx +269 -0
  92. package/src/feature-plugins/system/which-key.tsx +608 -0
  93. package/src/index.tsx +1 -0
  94. package/src/keymap.tsx +290 -0
  95. package/src/logo.ts +11 -0
  96. package/src/parsers-config.ts +386 -0
  97. package/src/plugin/adapters.tsx +355 -0
  98. package/src/plugin/api.ts +52 -0
  99. package/src/plugin/command-shim.ts +109 -0
  100. package/src/plugin/runtime.tsx +81 -0
  101. package/src/plugin/slots.tsx +65 -0
  102. package/src/prompt/display.ts +48 -0
  103. package/src/prompt/frecency.tsx +80 -0
  104. package/src/prompt/history.tsx +111 -0
  105. package/src/prompt/part.ts +29 -0
  106. package/src/prompt/stash.tsx +89 -0
  107. package/src/prompt/traits.ts +29 -0
  108. package/src/routes/home/session-destination.tsx +41 -0
  109. package/src/routes/home.tsx +95 -0
  110. package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
  111. package/src/routes/session/dialog-message.tsx +109 -0
  112. package/src/routes/session/dialog-subagent.tsx +26 -0
  113. package/src/routes/session/dialog-timeline.tsx +47 -0
  114. package/src/routes/session/footer.tsx +91 -0
  115. package/src/routes/session/index.tsx +2710 -0
  116. package/src/routes/session/permission.tsx +718 -0
  117. package/src/routes/session/question.tsx +514 -0
  118. package/src/routes/session/sidebar.tsx +103 -0
  119. package/src/routes/session/subagent-footer.tsx +132 -0
  120. package/src/runtime.tsx +9 -0
  121. package/src/terminal-win32.ts +130 -0
  122. package/src/theme/assets/aura.json +69 -0
  123. package/src/theme/assets/ayu.json +80 -0
  124. package/src/theme/assets/carbonfox.json +248 -0
  125. package/src/theme/assets/catppuccin-frappe.json +230 -0
  126. package/src/theme/assets/catppuccin-macchiato.json +230 -0
  127. package/src/theme/assets/catppuccin.json +112 -0
  128. package/src/theme/assets/cobalt2.json +225 -0
  129. package/src/theme/assets/cursor.json +249 -0
  130. package/src/theme/assets/dracula.json +219 -0
  131. package/src/theme/assets/everforest.json +241 -0
  132. package/src/theme/assets/flexoki.json +237 -0
  133. package/src/theme/assets/github.json +233 -0
  134. package/src/theme/assets/gruvbox.json +242 -0
  135. package/src/theme/assets/kanagawa.json +77 -0
  136. package/src/theme/assets/lucent-orng.json +234 -0
  137. package/src/theme/assets/material.json +235 -0
  138. package/src/theme/assets/matrix.json +77 -0
  139. package/src/theme/assets/mercury.json +252 -0
  140. package/src/theme/assets/monokai.json +221 -0
  141. package/src/theme/assets/nightowl.json +221 -0
  142. package/src/theme/assets/nord.json +223 -0
  143. package/src/theme/assets/one-dark.json +84 -0
  144. package/src/theme/assets/opencode.json +245 -0
  145. package/src/theme/assets/orng.json +249 -0
  146. package/src/theme/assets/osaka-jade.json +93 -0
  147. package/src/theme/assets/palenight.json +222 -0
  148. package/src/theme/assets/rosepine.json +234 -0
  149. package/src/theme/assets/solarized.json +223 -0
  150. package/src/theme/assets/synthwave84.json +226 -0
  151. package/src/theme/assets/tokyonight.json +243 -0
  152. package/src/theme/assets/vercel.json +245 -0
  153. package/src/theme/assets/vesper.json +218 -0
  154. package/src/theme/assets/zenburn.json +223 -0
  155. package/src/theme/index.ts +1089 -0
  156. package/src/ui/border.ts +21 -0
  157. package/src/ui/dialog-alert.tsx +66 -0
  158. package/src/ui/dialog-confirm.tsx +108 -0
  159. package/src/ui/dialog-export-options.tsx +217 -0
  160. package/src/ui/dialog-help.tsx +40 -0
  161. package/src/ui/dialog-prompt.tsx +126 -0
  162. package/src/ui/dialog-select.tsx +790 -0
  163. package/src/ui/dialog.tsx +231 -0
  164. package/src/ui/link.tsx +34 -0
  165. package/src/ui/spinner.ts +368 -0
  166. package/src/ui/toast.tsx +102 -0
  167. package/src/util/collapse-tool-output.ts +19 -0
  168. package/src/util/error.ts +182 -0
  169. package/src/util/filetype.ts +130 -0
  170. package/src/util/format.ts +20 -0
  171. package/src/util/layout.ts +25 -0
  172. package/src/util/locale.ts +86 -0
  173. package/src/util/model.ts +28 -0
  174. package/src/util/path.ts +12 -0
  175. package/src/util/persistence.ts +33 -0
  176. package/src/util/presentation.ts +38 -0
  177. package/src/util/provider-origin.ts +7 -0
  178. package/src/util/record.ts +3 -0
  179. package/src/util/renderer.ts +7 -0
  180. package/src/util/revert-diff.ts +18 -0
  181. package/src/util/scroll.ts +27 -0
  182. package/src/util/selection.ts +79 -0
  183. package/src/util/session.ts +3 -0
  184. package/src/util/signal.ts +51 -0
  185. package/src/util/system.ts +20 -0
  186. package/src/util/tool-display.ts +13 -0
  187. package/src/util/transcript.ts +112 -0
  188. package/sst-env.d.ts +10 -0
  189. package/test/app-lifecycle.test.tsx +128 -0
  190. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  191. package/test/cli/cmd/tui/model-options.test.ts +32 -0
  192. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  193. package/test/cli/cmd/tui/provider-options.test.ts +41 -0
  194. package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
  195. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
  196. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
  197. package/test/cli/cmd/tui/sync.test.tsx +65 -0
  198. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
  199. package/test/cli/tui/data.test.tsx +486 -0
  200. package/test/cli/tui/dialog-prompt.test.tsx +147 -0
  201. package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
  202. package/test/cli/tui/diff-viewer.test.tsx +268 -0
  203. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
  204. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  205. package/test/cli/tui/thinking.test.ts +36 -0
  206. package/test/cli/tui/use-event.test.tsx +148 -0
  207. package/test/clipboard.test.ts +19 -0
  208. package/test/component/dialog-session-list.test.ts +46 -0
  209. package/test/config.test.tsx +127 -0
  210. package/test/context/local.test.ts +22 -0
  211. package/test/editor.test.ts +32 -0
  212. package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
  213. package/test/fixture/fixture.ts +13 -0
  214. package/test/fixture/tui-environment.tsx +32 -0
  215. package/test/fixture/tui-plugin.ts +36 -0
  216. package/test/fixture/tui-runtime.ts +12 -0
  217. package/test/fixture/tui-sdk.ts +109 -0
  218. package/test/index.test.tsx +6 -0
  219. package/test/keymap.test.tsx +141 -0
  220. package/test/plugin/runtime.test.ts +50 -0
  221. package/test/plugin/slots.test.tsx +38 -0
  222. package/test/prompt/display.test.ts +33 -0
  223. package/test/prompt/history.test.ts +39 -0
  224. package/test/prompt/jsonl.test.ts +24 -0
  225. package/test/prompt/local-attachment.test.ts +43 -0
  226. package/test/prompt/part.test.ts +53 -0
  227. package/test/prompt/persistence.test.ts +23 -0
  228. package/test/prompt/traits.test.ts +25 -0
  229. package/test/runtime.test.tsx +37 -0
  230. package/test/theme.test.ts +81 -0
  231. package/test/util/error.test.ts +49 -0
  232. package/test/util/filetype.test.ts +16 -0
  233. package/test/util/format.test.ts +59 -0
  234. package/test/util/model.test.ts +9 -0
  235. package/test/util/presentation.test.ts +8 -0
  236. package/test/util/renderer.test.ts +30 -0
  237. package/test/util/revert-diff.test.ts +35 -0
  238. package/test/util/session.test.ts +10 -0
  239. package/test/util/tool-display.test.ts +40 -0
  240. package/test/util/transcript.test.ts +421 -0
  241. package/tsconfig.json +10 -0
@@ -0,0 +1,514 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
3
+ import { useRenderer } from "@opentui/solid"
4
+ import type { TextareaRenderable } from "@opentui/core"
5
+ import { selectedForeground, tint, useTheme } from "../../context/theme"
6
+ import type { QuestionAnswer, QuestionRequest } from "@neurocode-ai/sdk/v2"
7
+ import { useSDK } from "../../context/sdk"
8
+ import { SplitBorder } from "../../ui/border"
9
+ import { useTuiConfig } from "../../config"
10
+ import { useBindings, useNeurocodeModeStack } from "../../keymap"
11
+
12
+ const QUESTION_MODE = "question"
13
+
14
+ export function QuestionPrompt(props: { request: QuestionRequest; directory?: string }) {
15
+ const sdk = useSDK()
16
+ const { theme } = useTheme()
17
+ const renderer = useRenderer()
18
+ const tuiConfig = useTuiConfig()
19
+ const modeStack = useNeurocodeModeStack()
20
+
21
+ const questions = createMemo(() => props.request.questions)
22
+ const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
23
+ const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select)
24
+ const [tabHover, setTabHover] = createSignal<number | "confirm" | null>(null)
25
+ const [store, setStore] = createStore({
26
+ tab: 0,
27
+ answers: [] as QuestionAnswer[],
28
+ custom: [] as string[],
29
+ selected: 0,
30
+ editing: false,
31
+ })
32
+
33
+ let textarea: TextareaRenderable | undefined
34
+
35
+ const question = createMemo(() => questions()[store.tab])
36
+ const confirm = createMemo(() => !single() && store.tab === questions().length)
37
+ const options = createMemo(() => question()?.options ?? [])
38
+ const custom = createMemo(() => question()?.custom !== false)
39
+ const other = createMemo(() => custom() && store.selected === options().length)
40
+ const input = createMemo(() => store.custom[store.tab] ?? "")
41
+ const multi = createMemo(() => question()?.multiple === true)
42
+ const customPicked = createMemo(() => {
43
+ const value = input()
44
+ if (!value) return false
45
+ return store.answers[store.tab]?.includes(value) ?? false
46
+ })
47
+
48
+ function submit() {
49
+ const answers = questions().map((_, i) => store.answers[i] ?? [])
50
+ void sdk.client.question.reply({
51
+ requestID: props.request.id,
52
+ directory: props.directory,
53
+ answers,
54
+ })
55
+ }
56
+
57
+ function reject() {
58
+ void sdk.client.question.reject({
59
+ requestID: props.request.id,
60
+ directory: props.directory,
61
+ })
62
+ }
63
+
64
+ function pick(answer: string, custom: boolean = false) {
65
+ const answers = [...store.answers]
66
+ answers[store.tab] = [answer]
67
+ setStore("answers", answers)
68
+ if (custom) {
69
+ const inputs = [...store.custom]
70
+ inputs[store.tab] = answer
71
+ setStore("custom", inputs)
72
+ }
73
+ if (single()) {
74
+ void sdk.client.question.reply({
75
+ requestID: props.request.id,
76
+ directory: props.directory,
77
+ answers: [[answer]],
78
+ })
79
+ return
80
+ }
81
+ setStore("tab", store.tab + 1)
82
+ setStore("selected", 0)
83
+ }
84
+
85
+ function toggle(answer: string) {
86
+ const existing = store.answers[store.tab] ?? []
87
+ const next = [...existing]
88
+ const index = next.indexOf(answer)
89
+ if (index === -1) next.push(answer)
90
+ if (index !== -1) next.splice(index, 1)
91
+ const answers = [...store.answers]
92
+ answers[store.tab] = next
93
+ setStore("answers", answers)
94
+ }
95
+
96
+ function moveTo(index: number) {
97
+ setStore("selected", index)
98
+ }
99
+
100
+ function selectTab(index: number) {
101
+ setStore("tab", index)
102
+ setStore("selected", 0)
103
+ }
104
+
105
+ function selectOption() {
106
+ if (other()) {
107
+ if (!multi()) {
108
+ setStore("editing", true)
109
+ return
110
+ }
111
+ const value = input()
112
+ if (value && customPicked()) {
113
+ toggle(value)
114
+ return
115
+ }
116
+ setStore("editing", true)
117
+ return
118
+ }
119
+ const opt = options()[store.selected]
120
+ if (!opt) return
121
+ if (multi()) {
122
+ toggle(opt.label)
123
+ return
124
+ }
125
+ pick(opt.label)
126
+ }
127
+
128
+ onMount(() => {
129
+ const popMode = modeStack.push(QUESTION_MODE)
130
+ onCleanup(popMode)
131
+ })
132
+
133
+ useBindings(() => ({
134
+ mode: QUESTION_MODE,
135
+ enabled: store.editing && !confirm(),
136
+ commands: [
137
+ {
138
+ name: "prompt.clear",
139
+ title: "Clear answer edit",
140
+ category: "Question",
141
+ run() {
142
+ const text = textarea?.plainText ?? ""
143
+ if (!text) {
144
+ setStore("editing", false)
145
+ return
146
+ }
147
+ textarea?.setText("")
148
+ },
149
+ },
150
+ ],
151
+ bindings: [
152
+ {
153
+ key: "escape",
154
+ desc: "Cancel answer edit",
155
+ group: "Question",
156
+ cmd: () => {
157
+ setStore("editing", false)
158
+ },
159
+ },
160
+ ...tuiConfig.keybinds.get("prompt.clear"),
161
+ {
162
+ key: "return",
163
+ desc: "Submit answer edit",
164
+ group: "Question",
165
+ cmd: () => {
166
+ const text = textarea?.plainText?.trim() ?? ""
167
+ const prev = store.custom[store.tab]
168
+
169
+ if (!text) {
170
+ if (prev) {
171
+ const inputs = [...store.custom]
172
+ inputs[store.tab] = ""
173
+ setStore("custom", inputs)
174
+
175
+ const answers = [...store.answers]
176
+ answers[store.tab] = (answers[store.tab] ?? []).filter((x) => x !== prev)
177
+ setStore("answers", answers)
178
+ }
179
+ setStore("editing", false)
180
+ return
181
+ }
182
+
183
+ if (multi()) {
184
+ const inputs = [...store.custom]
185
+ inputs[store.tab] = text
186
+ setStore("custom", inputs)
187
+
188
+ const existing = store.answers[store.tab] ?? []
189
+ const next = [...existing]
190
+ if (prev) {
191
+ const index = next.indexOf(prev)
192
+ if (index !== -1) next.splice(index, 1)
193
+ }
194
+ if (!next.includes(text)) next.push(text)
195
+ const answers = [...store.answers]
196
+ answers[store.tab] = next
197
+ setStore("answers", answers)
198
+ setStore("editing", false)
199
+ return
200
+ }
201
+
202
+ pick(text, true)
203
+ setStore("editing", false)
204
+ },
205
+ },
206
+ ],
207
+ }))
208
+
209
+ useBindings(() => {
210
+ const opts = options()
211
+ const total = opts.length + (custom() ? 1 : 0)
212
+ const max = Math.min(total, 9)
213
+
214
+ return {
215
+ mode: QUESTION_MODE,
216
+ enabled: !store.editing,
217
+ commands: [
218
+ {
219
+ name: "app.exit",
220
+ title: "Reject question",
221
+ category: "Question",
222
+ run() {
223
+ reject()
224
+ },
225
+ },
226
+ ],
227
+ bindings: [
228
+ {
229
+ key: "left",
230
+ desc: "Previous question",
231
+ group: "Question",
232
+ cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()),
233
+ },
234
+ {
235
+ key: "h",
236
+ desc: "Previous question",
237
+ group: "Question",
238
+ cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()),
239
+ },
240
+ { key: "right", desc: "Next question", group: "Question", cmd: () => selectTab((store.tab + 1) % tabs()) },
241
+ { key: "l", desc: "Next question", group: "Question", cmd: () => selectTab((store.tab + 1) % tabs()) },
242
+ {
243
+ key: "tab",
244
+ desc: "Next question",
245
+ group: "Question",
246
+ cmd: ({ event }: { event: { shift: boolean } }) => {
247
+ selectTab((store.tab + (event.shift ? -1 : 1) + tabs()) % tabs())
248
+ },
249
+ },
250
+ ...(confirm()
251
+ ? [
252
+ { key: "return", desc: "Submit answer", group: "Question", cmd: () => submit() },
253
+ { key: "escape", desc: "Reject question", group: "Question", cmd: () => reject() },
254
+ ...tuiConfig.keybinds.get("app.exit"),
255
+ ]
256
+ : [
257
+ ...Array.from({ length: max }, (_, index) => ({
258
+ key: String(index + 1),
259
+ desc: `Select answer ${index + 1}`,
260
+ group: "Question",
261
+ cmd: () => {
262
+ moveTo(index)
263
+ selectOption()
264
+ },
265
+ })),
266
+ {
267
+ key: "up",
268
+ desc: "Previous answer",
269
+ group: "Question",
270
+ cmd: () => moveTo((store.selected - 1 + total) % total),
271
+ },
272
+ {
273
+ key: "k",
274
+ desc: "Previous answer",
275
+ group: "Question",
276
+ cmd: () => moveTo((store.selected - 1 + total) % total),
277
+ },
278
+ { key: "down", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) },
279
+ { key: "j", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) },
280
+ { key: "return", desc: "Select answer", group: "Question", cmd: () => selectOption() },
281
+ { key: "escape", desc: "Reject question", group: "Question", cmd: () => reject() },
282
+ ...tuiConfig.keybinds.get("app.exit"),
283
+ ]),
284
+ ],
285
+ }
286
+ })
287
+
288
+ return (
289
+ <box
290
+ backgroundColor={theme.backgroundPanel}
291
+ border={["left"]}
292
+ borderColor={theme.accent}
293
+ customBorderChars={SplitBorder.customBorderChars}
294
+ >
295
+ <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
296
+ <Show when={!single()}>
297
+ <box flexDirection="row" gap={1} paddingLeft={1}>
298
+ <For each={questions()}>
299
+ {(q, index) => {
300
+ const isActive = () => index() === store.tab
301
+ const isAnswered = () => {
302
+ return (store.answers[index()]?.length ?? 0) > 0
303
+ }
304
+ return (
305
+ <box
306
+ paddingLeft={1}
307
+ paddingRight={1}
308
+ backgroundColor={
309
+ isActive()
310
+ ? theme.accent
311
+ : tabHover() === index()
312
+ ? theme.backgroundElement
313
+ : theme.backgroundPanel
314
+ }
315
+ onMouseOver={() => setTabHover(index())}
316
+ onMouseOut={() => setTabHover(null)}
317
+ onMouseUp={() => {
318
+ if (renderer.getSelection()?.getSelectedText()) return
319
+ selectTab(index())
320
+ }}
321
+ >
322
+ <text
323
+ fg={
324
+ isActive()
325
+ ? selectedForeground(theme, theme.accent)
326
+ : isAnswered()
327
+ ? theme.text
328
+ : theme.textMuted
329
+ }
330
+ >
331
+ {q.header}
332
+ </text>
333
+ </box>
334
+ )
335
+ }}
336
+ </For>
337
+ <box
338
+ paddingLeft={1}
339
+ paddingRight={1}
340
+ backgroundColor={
341
+ confirm() ? theme.accent : tabHover() === "confirm" ? theme.backgroundElement : theme.backgroundPanel
342
+ }
343
+ onMouseOver={() => setTabHover("confirm")}
344
+ onMouseOut={() => setTabHover(null)}
345
+ onMouseUp={() => {
346
+ if (renderer.getSelection()?.getSelectedText()) return
347
+ selectTab(questions().length)
348
+ }}
349
+ >
350
+ <text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
351
+ </box>
352
+ </box>
353
+ </Show>
354
+
355
+ <Show when={!confirm()}>
356
+ <box paddingLeft={1} gap={1}>
357
+ <box>
358
+ <text fg={theme.text}>
359
+ {question()?.question}
360
+ {multi() ? " (select all that apply)" : ""}
361
+ </text>
362
+ </box>
363
+ <box>
364
+ <For each={options()}>
365
+ {(opt, i) => {
366
+ const active = () => i() === store.selected
367
+ const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
368
+ return (
369
+ <box
370
+ onMouseOver={() => moveTo(i())}
371
+ onMouseDown={() => moveTo(i())}
372
+ onMouseUp={() => {
373
+ if (renderer.getSelection()?.getSelectedText()) return
374
+ selectOption()
375
+ }}
376
+ >
377
+ <box flexDirection="row">
378
+ <box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
379
+ <text fg={active() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
380
+ {`${i() + 1}.`}
381
+ </text>
382
+ </box>
383
+ <box backgroundColor={active() ? theme.backgroundElement : undefined}>
384
+ <text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
385
+ {multi() ? `[${picked() ? "✓" : " "}] ${opt.label}` : opt.label}
386
+ </text>
387
+ </box>
388
+ <Show when={!multi()}>
389
+ <text fg={theme.success}>{picked() ? " ✓" : ""}</text>
390
+ </Show>
391
+ </box>
392
+
393
+ <box paddingLeft={3}>
394
+ <text fg={theme.textMuted}>{opt.description}</text>
395
+ </box>
396
+ </box>
397
+ )
398
+ }}
399
+ </For>
400
+ <Show when={custom()}>
401
+ <box
402
+ onMouseOver={() => moveTo(options().length)}
403
+ onMouseDown={() => moveTo(options().length)}
404
+ onMouseUp={() => {
405
+ if (renderer.getSelection()?.getSelectedText()) return
406
+ selectOption()
407
+ }}
408
+ >
409
+ <box flexDirection="row">
410
+ <box backgroundColor={other() ? theme.backgroundElement : undefined} paddingRight={1}>
411
+ <text fg={other() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
412
+ {`${options().length + 1}.`}
413
+ </text>
414
+ </box>
415
+ <box backgroundColor={other() ? theme.backgroundElement : undefined}>
416
+ <text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
417
+ {multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
418
+ </text>
419
+ </box>
420
+
421
+ <Show when={!multi()}>
422
+ <text fg={theme.success}>{customPicked() ? " ✓" : ""}</text>
423
+ </Show>
424
+ </box>
425
+ <Show when={store.editing}>
426
+ <box paddingLeft={3}>
427
+ <textarea
428
+ ref={(val: TextareaRenderable) => {
429
+ textarea = val
430
+ val.traits = { status: "ANSWER" }
431
+ queueMicrotask(() => {
432
+ val.focus()
433
+ val.gotoLineEnd()
434
+ })
435
+ }}
436
+ initialValue={input()}
437
+ placeholder="Type your own answer"
438
+ placeholderColor={theme.textMuted}
439
+ minHeight={1}
440
+ maxHeight={6}
441
+ textColor={theme.text}
442
+ focusedTextColor={theme.text}
443
+ cursorColor={theme.primary}
444
+ />
445
+ </box>
446
+ </Show>
447
+ <Show when={!store.editing && input()}>
448
+ <box paddingLeft={3}>
449
+ <text fg={theme.textMuted}>{input()}</text>
450
+ </box>
451
+ </Show>
452
+ </box>
453
+ </Show>
454
+ </box>
455
+ </box>
456
+ </Show>
457
+
458
+ <Show when={confirm() && !single()}>
459
+ <box paddingLeft={1}>
460
+ <text fg={theme.text}>Review</text>
461
+ </box>
462
+ <For each={questions()}>
463
+ {(q, index) => {
464
+ const value = () => store.answers[index()]?.join(", ") ?? ""
465
+ const answered = () => Boolean(value())
466
+ return (
467
+ <box paddingLeft={1}>
468
+ <text>
469
+ <span style={{ fg: theme.textMuted }}>{q.header}:</span>{" "}
470
+ <span style={{ fg: answered() ? theme.text : theme.error }}>
471
+ {answered() ? value() : "(not answered)"}
472
+ </span>
473
+ </text>
474
+ </box>
475
+ )
476
+ }}
477
+ </For>
478
+ </Show>
479
+ </box>
480
+ <box
481
+ flexDirection="row"
482
+ flexShrink={0}
483
+ gap={1}
484
+ paddingLeft={2}
485
+ paddingRight={3}
486
+ paddingBottom={1}
487
+ justifyContent="space-between"
488
+ >
489
+ <box flexDirection="row" gap={2}>
490
+ <Show when={!single()}>
491
+ <text fg={theme.text}>
492
+ {"⇆"} <span style={{ fg: theme.textMuted }}>tab</span>
493
+ </text>
494
+ </Show>
495
+ <Show when={!confirm()}>
496
+ <text fg={theme.text}>
497
+ {"↑↓"} <span style={{ fg: theme.textMuted }}>select</span>
498
+ </text>
499
+ </Show>
500
+ <text fg={theme.text}>
501
+ enter{" "}
502
+ <span style={{ fg: theme.textMuted }}>
503
+ {confirm() ? "submit" : multi() ? "toggle" : single() ? "submit" : "confirm"}
504
+ </span>
505
+ </text>
506
+
507
+ <text fg={theme.text}>
508
+ esc <span style={{ fg: theme.textMuted }}>dismiss</span>
509
+ </text>
510
+ </box>
511
+ </box>
512
+ </box>
513
+ )
514
+ }
@@ -0,0 +1,103 @@
1
+ import { useProject } from "../../context/project"
2
+ import { useSync } from "../../context/sync"
3
+ import { createMemo, Show } from "solid-js"
4
+ import { useTheme } from "../../context/theme"
5
+ import { useTuiConfig } from "../../config"
6
+ import { InstallationChannel, InstallationVersion } from "@neurocode-ai/core/installation/version"
7
+ import { usePluginRuntime } from "../../plugin/runtime"
8
+
9
+ import { getScrollAcceleration } from "../../util/scroll"
10
+ import { WorkspaceLabel } from "../../component/workspace-label"
11
+
12
+ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
13
+ const pluginRuntime = usePluginRuntime()
14
+ const project = useProject()
15
+ const sync = useSync()
16
+ const { theme } = useTheme()
17
+ const tuiConfig = useTuiConfig()
18
+ const session = createMemo(() => sync.session.get(props.sessionID))
19
+ const workspace = () => {
20
+ const workspaceID = session()?.workspaceID
21
+ if (!workspaceID) return
22
+ return project.workspace.get(workspaceID)
23
+ }
24
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
25
+
26
+ return (
27
+ <Show when={session()}>
28
+ <box
29
+ backgroundColor={theme.backgroundPanel}
30
+ width={42}
31
+ height="100%"
32
+ paddingTop={1}
33
+ paddingBottom={1}
34
+ paddingLeft={2}
35
+ paddingRight={2}
36
+ position={props.overlay ? "absolute" : "relative"}
37
+ >
38
+ <scrollbox
39
+ flexGrow={1}
40
+ scrollAcceleration={scrollAcceleration()}
41
+ verticalScrollbarOptions={{
42
+ trackOptions: {
43
+ backgroundColor: theme.background,
44
+ foregroundColor: theme.borderActive,
45
+ },
46
+ }}
47
+ >
48
+ <box flexShrink={0} gap={1} paddingRight={1}>
49
+ <pluginRuntime.Slot
50
+ name="sidebar_title"
51
+ mode="single_winner"
52
+ session_id={props.sessionID}
53
+ title={session()!.title}
54
+ share_url={session()!.share?.url}
55
+ >
56
+ <box paddingRight={1}>
57
+ <text fg={theme.text}>
58
+ <b>{session()!.title}</b>
59
+ </text>
60
+ <Show when={InstallationChannel !== "latest"}>
61
+ <text fg={theme.textMuted}>{props.sessionID}</text>
62
+ </Show>
63
+ <Show when={session()!.workspaceID}>
64
+ <text fg={theme.textMuted}>
65
+ <Show
66
+ when={workspace()}
67
+ fallback={<WorkspaceLabel type="unknown" name={session()!.workspaceID!} status="error" icon />}
68
+ >
69
+ {(item) => (
70
+ <WorkspaceLabel
71
+ type={item().type}
72
+ name={item().name}
73
+ status={project.workspace.status(item().id) ?? "error"}
74
+ icon
75
+ />
76
+ )}
77
+ </Show>
78
+ </text>
79
+ </Show>
80
+ <Show when={session()!.share?.url}>
81
+ <text fg={theme.textMuted}>{session()!.share!.url}</text>
82
+ </Show>
83
+ </box>
84
+ </pluginRuntime.Slot>
85
+ <pluginRuntime.Slot name="sidebar_content" session_id={props.sessionID} />
86
+ </box>
87
+ </scrollbox>
88
+
89
+ <box flexShrink={0} gap={1} paddingTop={1}>
90
+ <pluginRuntime.Slot name="sidebar_footer" mode="single_winner" session_id={props.sessionID}>
91
+ <text fg={theme.textMuted}>
92
+ <span style={{ fg: theme.success }}>•</span> <b>Open</b>
93
+ <span style={{ fg: theme.text }}>
94
+ <b>Code</b>
95
+ </span>{" "}
96
+ <span>{InstallationVersion}</span>
97
+ </text>
98
+ </pluginRuntime.Slot>
99
+ </box>
100
+ </box>
101
+ </Show>
102
+ )
103
+ }