@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,542 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+ import { batch, createEffect, createMemo } from "solid-js"
4
+ import { useSync } from "./sync"
5
+ import { useEvent } from "./event"
6
+ import path from "path"
7
+ import { useTuiPaths } from "./runtime"
8
+ import { useArgs } from "./args"
9
+ import { useSDK } from "./sdk"
10
+ import { RGBA } from "@opentui/core"
11
+ import { readJson, writeJsonAtomic } from "../util/persistence"
12
+ import { useTheme } from "./theme"
13
+ import { useToast } from "../ui/toast"
14
+ import { useRoute } from "./route"
15
+ import { usePermission } from "./permission"
16
+
17
+ export type LocalTheme = {
18
+ secondary: RGBA
19
+ accent: RGBA
20
+ success: RGBA
21
+ warning: RGBA
22
+ primary: RGBA
23
+ error: RGBA
24
+ info: RGBA
25
+ }
26
+
27
+ export function parseModel(model: string) {
28
+ const [providerID, ...rest] = model.split("/")
29
+ return {
30
+ providerID: providerID,
31
+ modelID: rest.join("/"),
32
+ }
33
+ }
34
+
35
+ export function recentModels(
36
+ model: { providerID: string; modelID: string },
37
+ recent: { providerID: string; modelID: string }[],
38
+ ) {
39
+ const seen = new Set<string>()
40
+ return [model, ...recent]
41
+ .filter((item) => {
42
+ const key = `${item.providerID}/${item.modelID}`
43
+ if (seen.has(key)) return false
44
+ seen.add(key)
45
+ return true
46
+ })
47
+ .slice(0, 10)
48
+ .map((item) => ({ providerID: item.providerID, modelID: item.modelID }))
49
+ }
50
+
51
+ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
52
+ name: "Local",
53
+ init: () => {
54
+ const sync = useSync()
55
+ const sdk = useSDK()
56
+ const toast = useToast()
57
+ const theme = useTheme().theme
58
+ const route = useRoute()
59
+ const paths = useTuiPaths()
60
+ const args = useArgs()
61
+ const event = useEvent()
62
+ const permission = usePermission()
63
+
64
+ function isModelValid(model: { providerID: string; modelID: string }) {
65
+ const provider = sync.data.provider.find((item) => item.id === model.providerID)
66
+ return !!provider?.models[model.modelID]
67
+ }
68
+
69
+ function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
70
+ for (const modelFn of modelFns) {
71
+ const model = modelFn()
72
+ if (!model) continue
73
+ if (isModelValid(model)) return model
74
+ }
75
+ }
76
+
77
+ function createAgent() {
78
+ const agents = createMemo(() => sync.data.agent.filter((agent) => agent.mode !== "subagent" && !agent.hidden))
79
+ const visibleAgents = createMemo(() => sync.data.agent.filter((agent) => !agent.hidden))
80
+ const [agentStore, setAgentStore] = createStore({
81
+ current: undefined as string | undefined,
82
+ })
83
+ const colors = createMemo(() => [
84
+ theme.secondary,
85
+ theme.accent,
86
+ theme.success,
87
+ theme.warning,
88
+ theme.primary,
89
+ theme.error,
90
+ theme.info,
91
+ ])
92
+ return {
93
+ list() {
94
+ return agents()
95
+ },
96
+ current() {
97
+ return agents().find((x) => x.name === agentStore.current) ?? agents().at(0)
98
+ },
99
+ set(name: string) {
100
+ if (!agents().some((x) => x.name === name))
101
+ return toast.show({
102
+ variant: "warning",
103
+ message: `Agent not found: ${name}`,
104
+ duration: 3000,
105
+ })
106
+ setAgentStore("current", name)
107
+ },
108
+ move(direction: 1 | -1) {
109
+ batch(() => {
110
+ const current = this.current()
111
+ if (!current) return
112
+ let next = agents().findIndex((x) => x.name === current.name) + direction
113
+ if (next < 0) next = agents().length - 1
114
+ if (next >= agents().length) next = 0
115
+ const value = agents()[next]
116
+ setAgentStore("current", value.name)
117
+ })
118
+ },
119
+ color(name: string) {
120
+ const index = visibleAgents().findIndex((x) => x.name === name)
121
+ if (index === -1) return colors()[0]
122
+ const agent = visibleAgents()[index]
123
+
124
+ if (agent?.color) {
125
+ const color = agent.color
126
+ if (color.startsWith("#")) return RGBA.fromHex(color)
127
+ // already validated by config, just satisfying TS here
128
+ return theme[color as keyof typeof theme] as RGBA
129
+ }
130
+ return colors()[index % colors().length]
131
+ },
132
+ }
133
+ }
134
+
135
+ const agent = createAgent()
136
+
137
+ function createModel() {
138
+ const [modelStore, setModelStore] = createStore<{
139
+ ready: boolean
140
+ model: Record<
141
+ string,
142
+ {
143
+ providerID: string
144
+ modelID: string
145
+ }
146
+ >
147
+ recent: {
148
+ providerID: string
149
+ modelID: string
150
+ }[]
151
+ favorite: {
152
+ providerID: string
153
+ modelID: string
154
+ }[]
155
+ variant: Record<string, string | undefined>
156
+ }>({
157
+ ready: false,
158
+ model: {},
159
+ recent: [],
160
+ favorite: [],
161
+ variant: {},
162
+ })
163
+
164
+ const filePath = path.join(paths.state, "model.json")
165
+ const state = {
166
+ pending: false,
167
+ }
168
+
169
+ function save() {
170
+ if (!modelStore.ready) {
171
+ state.pending = true
172
+ return
173
+ }
174
+ state.pending = false
175
+ void writeJsonAtomic(filePath, {
176
+ recent: modelStore.recent,
177
+ favorite: modelStore.favorite,
178
+ variant: modelStore.variant,
179
+ })
180
+ }
181
+
182
+ readJson<unknown>(filePath)
183
+ .then((x) => {
184
+ if (!x || typeof x !== "object") return
185
+ const value = x as Record<string, unknown>
186
+ if (Array.isArray(value.recent)) setModelStore("recent", value.recent)
187
+ if (Array.isArray(value.favorite)) setModelStore("favorite", value.favorite)
188
+ if (typeof value.variant === "object" && value.variant !== null)
189
+ setModelStore("variant", value.variant as Record<string, string | undefined>)
190
+ })
191
+ .catch(() => {})
192
+ .finally(() => {
193
+ setModelStore("ready", true)
194
+ if (state.pending) save()
195
+ })
196
+
197
+ const fallbackModel = createMemo(() => {
198
+ if (args.model) {
199
+ const { providerID, modelID } = parseModel(args.model)
200
+ if (isModelValid({ providerID, modelID })) {
201
+ return {
202
+ providerID,
203
+ modelID,
204
+ }
205
+ }
206
+ }
207
+
208
+ if (sync.data.config.model) {
209
+ const { providerID, modelID } = parseModel(sync.data.config.model)
210
+ if (isModelValid({ providerID, modelID })) {
211
+ return {
212
+ providerID,
213
+ modelID,
214
+ }
215
+ }
216
+ }
217
+
218
+ for (const item of modelStore.recent) {
219
+ if (isModelValid(item)) {
220
+ return item
221
+ }
222
+ }
223
+
224
+ const provider = sync.data.provider[0]
225
+ if (!provider) return undefined
226
+ const defaultModel = sync.data.provider_default[provider.id]
227
+ const firstModel = Object.values(provider.models)[0]
228
+ const model = defaultModel ?? firstModel?.id
229
+ if (!model) return undefined
230
+ return {
231
+ providerID: provider.id,
232
+ modelID: model,
233
+ }
234
+ })
235
+
236
+ const currentModel = createMemo(() => {
237
+ const a = agent.current()
238
+ return (
239
+ getFirstValidModel(
240
+ () => a && modelStore.model[a.name],
241
+ () => a && a.model,
242
+ fallbackModel,
243
+ ) ?? undefined
244
+ )
245
+ })
246
+
247
+ return {
248
+ current: currentModel,
249
+ get ready() {
250
+ return modelStore.ready
251
+ },
252
+ recent() {
253
+ return modelStore.recent
254
+ },
255
+ favorite() {
256
+ return modelStore.favorite
257
+ },
258
+ parsed: createMemo(() => {
259
+ const value = currentModel()
260
+ if (!value) {
261
+ return {
262
+ provider: "Connect a provider",
263
+ model: "No provider selected",
264
+ reasoning: false,
265
+ }
266
+ }
267
+ const provider = sync.data.provider.find((item) => item.id === value.providerID)
268
+ const info = provider?.models[value.modelID]
269
+ return {
270
+ provider: provider?.name ?? value.providerID,
271
+ model: info?.name ?? value.modelID,
272
+ reasoning: info?.capabilities?.reasoning ?? false,
273
+ }
274
+ }),
275
+ cycle(direction: 1 | -1) {
276
+ const current = currentModel()
277
+ if (!current) return
278
+ const recent = modelStore.recent
279
+ const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
280
+ if (index === -1) return
281
+ let next = index + direction
282
+ if (next < 0) next = recent.length - 1
283
+ if (next >= recent.length) next = 0
284
+ const val = recent[next]
285
+ if (!val) return
286
+ const a = agent.current()
287
+ if (!a) return
288
+ setModelStore("model", a.name, { ...val })
289
+ },
290
+ cycleFavorite(direction: 1 | -1) {
291
+ const favorites = modelStore.favorite.filter((item) => isModelValid(item))
292
+ if (!favorites.length) {
293
+ toast.show({
294
+ variant: "info",
295
+ message: "Add a favorite model to use this shortcut",
296
+ duration: 3000,
297
+ })
298
+ return
299
+ }
300
+ const current = currentModel()
301
+ let index = -1
302
+ if (current) {
303
+ index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
304
+ }
305
+ if (index === -1) {
306
+ index = direction === 1 ? 0 : favorites.length - 1
307
+ } else {
308
+ index += direction
309
+ if (index < 0) index = favorites.length - 1
310
+ if (index >= favorites.length) index = 0
311
+ }
312
+ const next = favorites[index]
313
+ if (!next) return
314
+ const a = agent.current()
315
+ if (!a) return
316
+ setModelStore("model", a.name, { ...next })
317
+ setModelStore("recent", recentModels(next, modelStore.recent))
318
+ save()
319
+ },
320
+ set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
321
+ batch(() => {
322
+ if (!isModelValid(model)) {
323
+ toast.show({
324
+ message: `Model ${model.providerID}/${model.modelID} is not valid`,
325
+ variant: "warning",
326
+ duration: 3000,
327
+ })
328
+ return
329
+ }
330
+ const a = agent.current()
331
+ if (!a) return
332
+ setModelStore("model", a.name, model)
333
+ if (options?.recent) {
334
+ setModelStore("recent", recentModels(model, modelStore.recent))
335
+ save()
336
+ }
337
+ })
338
+ },
339
+ toggleFavorite(model: { providerID: string; modelID: string }) {
340
+ batch(() => {
341
+ if (!isModelValid(model)) {
342
+ toast.show({
343
+ message: `Model ${model.providerID}/${model.modelID} is not valid`,
344
+ variant: "warning",
345
+ duration: 3000,
346
+ })
347
+ return
348
+ }
349
+ const exists = modelStore.favorite.some(
350
+ (x) => x.providerID === model.providerID && x.modelID === model.modelID,
351
+ )
352
+ const next = exists
353
+ ? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
354
+ : [model, ...modelStore.favorite]
355
+ setModelStore(
356
+ "favorite",
357
+ next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
358
+ )
359
+ save()
360
+ })
361
+ },
362
+ variant: {
363
+ selected() {
364
+ const m = currentModel()
365
+ if (!m) return undefined
366
+ const key = `${m.providerID}/${m.modelID}`
367
+ return modelStore.variant[key]
368
+ },
369
+ current() {
370
+ const v = this.selected()
371
+ if (!v) return undefined
372
+ if (!this.list().includes(v)) return undefined
373
+ return v
374
+ },
375
+ list() {
376
+ const m = currentModel()
377
+ if (!m) return []
378
+ const provider = sync.data.provider.find((item) => item.id === m.providerID)
379
+ const info = provider?.models[m.modelID]
380
+ if (!info?.variants) return []
381
+ return Object.keys(info.variants)
382
+ },
383
+ set(value: string | undefined) {
384
+ const m = currentModel()
385
+ if (!m) return
386
+ const key = `${m.providerID}/${m.modelID}`
387
+ setModelStore("variant", key, value ?? "default")
388
+ save()
389
+ },
390
+ cycle() {
391
+ const variants = this.list()
392
+ if (variants.length === 0) return
393
+ const current = this.current()
394
+ if (!current) {
395
+ this.set(variants[0])
396
+ return
397
+ }
398
+ const index = variants.indexOf(current)
399
+ if (index === -1 || index === variants.length - 1) {
400
+ this.set(undefined)
401
+ return
402
+ }
403
+ this.set(variants[index + 1])
404
+ },
405
+ },
406
+ }
407
+ }
408
+
409
+ const model = createModel()
410
+
411
+ function createSession() {
412
+ const [sessionStore, setSessionStore] = createStore<{
413
+ ready: boolean
414
+ pinned: string[]
415
+ }>({
416
+ ready: false,
417
+ pinned: [],
418
+ })
419
+
420
+ const filePath = path.join(paths.state, "session.json")
421
+ const state = {
422
+ pending: false,
423
+ }
424
+
425
+ function save() {
426
+ if (!sessionStore.ready) {
427
+ state.pending = true
428
+ return
429
+ }
430
+ state.pending = false
431
+ void writeJsonAtomic(filePath, {
432
+ pinned: sessionStore.pinned,
433
+ })
434
+ }
435
+
436
+ readJson<unknown>(filePath)
437
+ .then((x) => {
438
+ if (!x || typeof x !== "object") return
439
+ const pinned = (x as Record<string, unknown>).pinned
440
+ if (Array.isArray(pinned))
441
+ setSessionStore(
442
+ "pinned",
443
+ pinned.filter((item): item is string => typeof item === "string"),
444
+ )
445
+ })
446
+ .catch(() => {})
447
+ .finally(() => {
448
+ setSessionStore("ready", true)
449
+ if (state.pending) save()
450
+ })
451
+
452
+ const slots = createMemo(() => {
453
+ const existing = new Set(sync.data.session.filter((x) => x.parentID === undefined).map((x) => x.id))
454
+ return sessionStore.pinned.filter((id) => existing.has(id)).slice(0, 9)
455
+ })
456
+
457
+ function prune(sessionID: string) {
458
+ batch(() => {
459
+ if (sessionStore.pinned.includes(sessionID)) {
460
+ setSessionStore(
461
+ "pinned",
462
+ sessionStore.pinned.filter((x) => x !== sessionID),
463
+ )
464
+ }
465
+ save()
466
+ })
467
+ }
468
+
469
+ event.on("session.deleted", (evt) => {
470
+ prune(evt.properties.info.id)
471
+ })
472
+
473
+ return {
474
+ get ready() {
475
+ return sessionStore.ready
476
+ },
477
+ pinned() {
478
+ return sessionStore.pinned
479
+ },
480
+ slots,
481
+ isPinned(sessionID: string) {
482
+ return sessionStore.pinned.includes(sessionID)
483
+ },
484
+ togglePin(sessionID: string) {
485
+ batch(() => {
486
+ const exists = sessionStore.pinned.includes(sessionID)
487
+ const next = exists
488
+ ? sessionStore.pinned.filter((x) => x !== sessionID)
489
+ : [...sessionStore.pinned, sessionID]
490
+ setSessionStore("pinned", next)
491
+ save()
492
+ })
493
+ },
494
+ quickSwitch(slot: number) {
495
+ const target = slots()[slot - 1]
496
+ if (!target) return
497
+ if (route.data.type === "session" && route.data.sessionID === target) return
498
+ route.navigate({ type: "session", sessionID: target })
499
+ },
500
+ }
501
+ }
502
+
503
+ const session = createSession()
504
+
505
+ const mcp = {
506
+ isEnabled(name: string) {
507
+ const status = sync.data.mcp[name]
508
+ return status?.status === "connected"
509
+ },
510
+ async toggle(name: string) {
511
+ const status = sync.data.mcp[name]
512
+ if (status?.status === "connected") {
513
+ // Disable: disconnect the MCP
514
+ await sdk.client.mcp.disconnect({ name })
515
+ } else {
516
+ // Enable/Retry: connect the MCP (handles disabled, failed, and other states)
517
+ await sdk.client.mcp.connect({ name })
518
+ }
519
+ },
520
+ }
521
+
522
+ createEffect(() => {
523
+ const value = agent.current()
524
+ if (!value?.model) return
525
+ if (isModelValid(value.model)) return
526
+ toast.show({
527
+ variant: "warning",
528
+ message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
529
+ duration: 3000,
530
+ })
531
+ })
532
+
533
+ const result = {
534
+ model,
535
+ agent,
536
+ mcp,
537
+ session,
538
+ permission,
539
+ }
540
+ return result
541
+ },
542
+ })
@@ -0,0 +1,14 @@
1
+ import type { LocationRef } from "@neurocode-ai/sdk/v2"
2
+ import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
3
+
4
+ const context = createContext<Accessor<LocationRef | undefined>>()
5
+
6
+ export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) {
7
+ return <context.Provider value={() => props.location}>{props.children}</context.Provider>
8
+ }
9
+
10
+ export function useLocation() {
11
+ const value = useContext(context)
12
+ if (!value) throw new Error("Location context must be used within a LocationProvider")
13
+ return value
14
+ }
@@ -0,0 +1,24 @@
1
+ import path from "path"
2
+ import { abbreviateHome } from "../runtime"
3
+ import { useLocation } from "./location"
4
+ import { useTuiPaths } from "./runtime"
5
+
6
+ export function usePathFormatter() {
7
+ const paths = useTuiPaths()
8
+ const location = useLocation()
9
+ return {
10
+ path: () => location()?.directory || paths.cwd,
11
+ format: (input?: string) => formatPath(input, location()?.directory || paths.cwd, paths.home),
12
+ }
13
+ }
14
+
15
+ function formatPath(input: string | undefined, base: string, home: string) {
16
+ if (typeof input !== "string" || !input) return ""
17
+
18
+ const absolute = path.isAbsolute(input) ? input : path.resolve(base, input)
19
+ const relative = path.relative(base, absolute)
20
+
21
+ if (!relative) return "."
22
+ if (relative !== ".." && !relative.startsWith(".." + path.sep)) return relative
23
+ return abbreviateHome(absolute, home)
24
+ }
@@ -0,0 +1,26 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { useArgs } from "./args"
3
+ import { createSimpleContext } from "./helper"
4
+
5
+ export type PermissionMode = "auto" | "normal"
6
+
7
+ export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
8
+ name: "Permission",
9
+ init: () => {
10
+ const args = useArgs()
11
+ const [store, setStore] = createStore<{ mode: PermissionMode }>({
12
+ mode: args.auto ? "auto" : "normal",
13
+ })
14
+ return {
15
+ get mode() {
16
+ return store.mode
17
+ },
18
+ set(mode: PermissionMode) {
19
+ setStore("mode", mode)
20
+ },
21
+ toggle() {
22
+ setStore("mode", (mode) => (mode === "auto" ? "normal" : "auto"))
23
+ },
24
+ }
25
+ },
26
+ })