@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,102 @@
1
+ import { createContext, useContext, type ParentProps, Show } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { useTheme } from "../context/theme"
4
+ import { useTerminalDimensions } from "@opentui/solid"
5
+ import { SplitBorder } from "./border"
6
+ import { TextAttributes } from "@opentui/core"
7
+ export type ToastOptions = {
8
+ title?: string
9
+ message: string
10
+ variant: "info" | "success" | "warning" | "error"
11
+ duration: number
12
+ }
13
+ type ToastInput = Omit<ToastOptions, "duration"> & { duration?: number }
14
+
15
+ export function Toast() {
16
+ const toast = useToast()
17
+ const { theme } = useTheme()
18
+ const dimensions = useTerminalDimensions()
19
+
20
+ return (
21
+ <Show when={toast.currentToast}>
22
+ {(current) => (
23
+ <box
24
+ position="absolute"
25
+ justifyContent="center"
26
+ alignItems="flex-start"
27
+ top={2}
28
+ right={2}
29
+ maxWidth={Math.min(60, dimensions().width - 6)}
30
+ paddingLeft={2}
31
+ paddingRight={2}
32
+ paddingTop={1}
33
+ paddingBottom={1}
34
+ backgroundColor={theme.backgroundPanel}
35
+ borderColor={theme[current().variant]}
36
+ border={["left", "right"]}
37
+ customBorderChars={SplitBorder.customBorderChars}
38
+ >
39
+ <Show when={current().title}>
40
+ <text attributes={TextAttributes.BOLD} marginBottom={1} fg={theme.text}>
41
+ {current().title}
42
+ </text>
43
+ </Show>
44
+ <text fg={theme.text} wrapMode="word" width="100%">
45
+ {current().message}
46
+ </text>
47
+ </box>
48
+ )}
49
+ </Show>
50
+ )
51
+ }
52
+
53
+ function init() {
54
+ const [store, setStore] = createStore({
55
+ currentToast: null as ToastOptions | null,
56
+ })
57
+
58
+ let timeoutHandle: NodeJS.Timeout | null = null
59
+
60
+ const toast = {
61
+ show(options: ToastInput) {
62
+ const toastOptions = { ...options, duration: options.duration ?? 5000 }
63
+ setStore("currentToast", toastOptions)
64
+ if (timeoutHandle) clearTimeout(timeoutHandle)
65
+ timeoutHandle = setTimeout(() => {
66
+ setStore("currentToast", null)
67
+ }, toastOptions.duration).unref()
68
+ },
69
+ error: (err: any) => {
70
+ if (err instanceof Error)
71
+ return toast.show({
72
+ variant: "error",
73
+ message: err.message,
74
+ })
75
+ toast.show({
76
+ variant: "error",
77
+ message: "An unknown error has occurred",
78
+ })
79
+ },
80
+ get currentToast(): ToastOptions | null {
81
+ return store.currentToast
82
+ },
83
+ }
84
+ return toast
85
+ }
86
+
87
+ export type ToastContext = ReturnType<typeof init>
88
+
89
+ const ctx = createContext<ToastContext>()
90
+
91
+ export function ToastProvider(props: ParentProps) {
92
+ const value = init()
93
+ return <ctx.Provider value={value}>{props.children}</ctx.Provider>
94
+ }
95
+
96
+ export function useToast() {
97
+ const value = useContext(ctx)
98
+ if (!value) {
99
+ throw new Error("useToast must be used within a ToastProvider")
100
+ }
101
+ return value
102
+ }
@@ -0,0 +1,19 @@
1
+ export function collapseToolOutput(output: string, maxLines: number, maxChars: number) {
2
+ const lines = output.split("\n")
3
+ if (lines.length <= maxLines && Array.from(output).length <= maxChars) {
4
+ return { output, overflow: false }
5
+ }
6
+
7
+ const preview = lines.slice(0, maxLines).join("\n")
8
+ if (Array.from(preview).length > maxChars) {
9
+ return {
10
+ output:
11
+ Array.from(preview)
12
+ .slice(0, Math.max(0, maxChars - 1))
13
+ .join("") + "…",
14
+ overflow: true,
15
+ }
16
+ }
17
+
18
+ return { output: [...lines.slice(0, maxLines), "…"].join("\n"), overflow: true }
19
+ }
@@ -0,0 +1,182 @@
1
+ import { isRecord } from "./record"
2
+
3
+ type ConfigIssue = { message: string; path: string[] }
4
+
5
+ export function cliErrorMessage(input: unknown): string | undefined {
6
+ if (input instanceof Error && isRecord(input.cause) && "body" in input.cause) {
7
+ const formatted = cliErrorMessage(input.cause.body)
8
+ if (formatted) return formatted
9
+ }
10
+
11
+ if (tagged(input, "CliError")) {
12
+ if (typeof input.exitCode === "number") process.exitCode = input.exitCode
13
+ return field(input, "message") ?? ""
14
+ }
15
+ if (tagged(input, "AccountServiceError") || tagged(input, "AccountTransportError")) {
16
+ return field(input, "message") ?? ""
17
+ }
18
+
19
+ const model = configData(input, "ProviderModelNotFoundError")
20
+ if (model) {
21
+ const suggestions = Array.isArray(model.suggestions)
22
+ ? model.suggestions.filter((item): item is string => typeof item === "string")
23
+ : []
24
+ return [
25
+ `Model not found: ${field(model, "providerID")}/${field(model, "modelID")}`,
26
+ ...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
27
+ "Try: `neurocode models` to list available models",
28
+ "Or check your config (opencode.json) provider/model names",
29
+ ].join("\n")
30
+ }
31
+
32
+ const provider = configData(input, "ProviderInitError")
33
+ if (provider)
34
+ return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.`
35
+
36
+ const json = configData(input, "ConfigJsonError")
37
+ if (json) {
38
+ const message = field(json, "message")
39
+ return `Config file at ${field(json, "path")} is not valid JSON(C)` + (message ? `: ${message}` : "")
40
+ }
41
+
42
+ const directory = configData(input, "ConfigDirectoryTypoError")
43
+ if (directory) {
44
+ return `Directory "${field(directory, "dir")}" in ${field(directory, "path")} is not valid. Rename the directory to "${field(directory, "suggestion")}" or remove it. This is a common typo.`
45
+ }
46
+
47
+ const frontmatter = configData(input, "ConfigFrontmatterError")
48
+ if (frontmatter) return field(frontmatter, "message") ?? ""
49
+
50
+ const invalid = configData(input, "ConfigInvalidError")
51
+ if (invalid) {
52
+ const path = field(invalid, "path")
53
+ const message = field(invalid, "message")
54
+ const issues = Array.isArray(invalid.issues)
55
+ ? invalid.issues.filter((issue): issue is ConfigIssue => {
56
+ return (
57
+ isRecord(issue) &&
58
+ typeof issue.message === "string" &&
59
+ Array.isArray(issue.path) &&
60
+ issue.path.every((item) => typeof item === "string")
61
+ )
62
+ })
63
+ : []
64
+ return [
65
+ `Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""),
66
+ ...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")),
67
+ ].join("\n")
68
+ }
69
+
70
+ if (tagged(input, "UICancelledError") || named(input, "UICancelledError")) return ""
71
+ if (isRecord(input) && named(input, "MCPFailed")) {
72
+ const name = isRecord(input.data) ? field(input.data, "name") : undefined
73
+ return `MCP server "${name}" failed. Note, neurocode does not support MCP authentication yet.`
74
+ }
75
+ return undefined
76
+ }
77
+
78
+ function tagged(input: unknown, tag: string): input is Record<string, unknown> {
79
+ return isRecord(input) && input._tag === tag
80
+ }
81
+
82
+ function named(input: unknown, name: string) {
83
+ return isRecord(input) && (input.name === name || input._tag === name)
84
+ }
85
+
86
+ function configData(input: unknown, tag: string) {
87
+ if (!isRecord(input)) return undefined
88
+ if (input.name === tag && isRecord(input.data)) return input.data
89
+ if (input._tag === tag) return input
90
+ return undefined
91
+ }
92
+
93
+ function field(input: Record<string, unknown>, key: string) {
94
+ return typeof input[key] === "string" ? input[key] : undefined
95
+ }
96
+
97
+ export function errorFormat(error: unknown): string {
98
+ if (error instanceof Error) {
99
+ return error.stack ?? `${error.name}: ${error.message}`
100
+ }
101
+
102
+ if (typeof error === "object" && error !== null) {
103
+ try {
104
+ const json = JSON.stringify(error, null, 2)
105
+ // Plain objects whose own properties are all non-enumerable (or empty)
106
+ // serialize to "{}", which prints as a useless bare `{}` on stderr.
107
+ // Fall back to a custom toString first, then to ctor name + own prop names.
108
+ if (json === "{}") {
109
+ const str = String(error)
110
+ if (str && str !== "[object Object]") return str
111
+ const ctor = error.constructor?.name
112
+ const prefix = ctor && ctor !== "Object" ? ctor : "Error"
113
+ const names = Object.getOwnPropertyNames(error)
114
+ return names.length === 0 ? `${prefix} (no message)` : `${prefix} { ${names.join(", ")} }`
115
+ }
116
+ return json
117
+ } catch {
118
+ return "Unexpected error (unserializable)"
119
+ }
120
+ }
121
+
122
+ return String(error)
123
+ }
124
+
125
+ export function errorMessage(error: unknown): string {
126
+ if (error instanceof Error) {
127
+ if (error.message) return error.message
128
+ if (error.name) return error.name
129
+ }
130
+
131
+ if (isRecord(error) && typeof error.message === "string" && error.message) {
132
+ return error.message
133
+ }
134
+
135
+ if (isRecord(error) && isRecord(error.data) && typeof error.data.message === "string" && error.data.message) {
136
+ return error.data.message
137
+ }
138
+
139
+ const text = String(error)
140
+ if (text && text !== "[object Object]") return text
141
+
142
+ const formatted = errorFormat(error)
143
+ if (formatted) return formatted
144
+ return "unknown error"
145
+ }
146
+
147
+ export function errorData(error: unknown) {
148
+ if (error instanceof Error) {
149
+ return {
150
+ type: error.name,
151
+ message: errorMessage(error),
152
+ stack: error.stack,
153
+ cause: error.cause === undefined ? undefined : errorFormat(error.cause),
154
+ formatted: errorFormat(error),
155
+ }
156
+ }
157
+
158
+ if (!isRecord(error)) {
159
+ return {
160
+ type: typeof error,
161
+ message: errorMessage(error),
162
+ formatted: errorFormat(error),
163
+ }
164
+ }
165
+
166
+ const data = Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => {
167
+ const value = error[key]
168
+ if (value === undefined) return acc
169
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
170
+ acc[key] = value
171
+ return acc
172
+ }
173
+ // oxlint-disable-next-line no-base-to-string -- intentional coercion of arbitrary error properties
174
+ acc[key] = value instanceof Error ? value.message : String(value)
175
+ return acc
176
+ }, {})
177
+
178
+ if (typeof data.message !== "string") data.message = errorMessage(error)
179
+ if (typeof data.type !== "string") data.type = error.constructor?.name
180
+ data.formatted = errorFormat(error)
181
+ return data
182
+ }
@@ -0,0 +1,130 @@
1
+ import path from "node:path"
2
+
3
+ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
4
+ ".abap": "abap",
5
+ ".bat": "bat",
6
+ ".bib": "bibtex",
7
+ ".bibtex": "bibtex",
8
+ ".clj": "clojure",
9
+ ".cljs": "clojure",
10
+ ".cljc": "clojure",
11
+ ".edn": "clojure",
12
+ ".coffee": "coffeescript",
13
+ ".c": "c",
14
+ ".cpp": "cpp",
15
+ ".cxx": "cpp",
16
+ ".cc": "cpp",
17
+ ".c++": "cpp",
18
+ ".cs": "csharp",
19
+ ".csx": "csharp",
20
+ ".css": "css",
21
+ ".d": "d",
22
+ ".pas": "pascal",
23
+ ".pascal": "pascal",
24
+ ".diff": "diff",
25
+ ".patch": "diff",
26
+ ".dart": "dart",
27
+ ".dockerfile": "dockerfile",
28
+ ".ex": "elixir",
29
+ ".exs": "elixir",
30
+ ".erl": "erlang",
31
+ ".ets": "typescript",
32
+ ".hrl": "erlang",
33
+ ".fs": "fsharp",
34
+ ".fsi": "fsharp",
35
+ ".fsx": "fsharp",
36
+ ".fsscript": "fsharp",
37
+ ".gitcommit": "git-commit",
38
+ ".gitrebase": "git-rebase",
39
+ ".go": "go",
40
+ ".groovy": "groovy",
41
+ ".gleam": "gleam",
42
+ ".hbs": "handlebars",
43
+ ".handlebars": "handlebars",
44
+ ".hs": "haskell",
45
+ ".lhs": "haskell",
46
+ ".html": "html",
47
+ ".htm": "html",
48
+ ".ini": "ini",
49
+ ".java": "java",
50
+ ".jl": "julia",
51
+ ".js": "javascript",
52
+ ".kt": "kotlin",
53
+ ".kts": "kotlin",
54
+ ".jsx": "javascriptreact",
55
+ ".json": "json",
56
+ ".tex": "latex",
57
+ ".latex": "latex",
58
+ ".less": "less",
59
+ ".lua": "lua",
60
+ ".makefile": "makefile",
61
+ makefile: "makefile",
62
+ ".md": "markdown",
63
+ ".markdown": "markdown",
64
+ ".m": "objective-c",
65
+ ".mm": "objective-cpp",
66
+ ".pl": "perl",
67
+ ".pm": "perl",
68
+ ".pm6": "perl6",
69
+ ".php": "php",
70
+ ".ps1": "powershell",
71
+ ".psm1": "powershell",
72
+ ".pug": "jade",
73
+ ".jade": "jade",
74
+ ".py": "python",
75
+ ".r": "r",
76
+ ".cshtml": "razor",
77
+ ".razor": "razor",
78
+ ".rb": "ruby",
79
+ ".rake": "ruby",
80
+ ".gemspec": "ruby",
81
+ ".ru": "ruby",
82
+ ".erb": "erb",
83
+ ".html.erb": "erb",
84
+ ".js.erb": "erb",
85
+ ".css.erb": "erb",
86
+ ".json.erb": "erb",
87
+ ".rs": "rust",
88
+ ".scss": "scss",
89
+ ".sass": "sass",
90
+ ".scala": "scala",
91
+ ".shader": "shaderlab",
92
+ ".sh": "shellscript",
93
+ ".bash": "shellscript",
94
+ ".zsh": "shellscript",
95
+ ".ksh": "shellscript",
96
+ ".sql": "sql",
97
+ ".svelte": "svelte",
98
+ ".swift": "swift",
99
+ ".ts": "typescript",
100
+ ".tsx": "typescriptreact",
101
+ ".mts": "typescript",
102
+ ".cts": "typescript",
103
+ ".mtsx": "typescriptreact",
104
+ ".ctsx": "typescriptreact",
105
+ ".xml": "xml",
106
+ ".xsl": "xsl",
107
+ ".yaml": "yaml",
108
+ ".yml": "yaml",
109
+ ".mjs": "javascript",
110
+ ".cjs": "javascript",
111
+ ".vue": "vue",
112
+ ".zig": "zig",
113
+ ".zon": "zig",
114
+ ".astro": "astro",
115
+ ".ml": "ocaml",
116
+ ".mli": "ocaml",
117
+ ".tf": "terraform",
118
+ ".tfvars": "terraform-vars",
119
+ ".hcl": "hcl",
120
+ ".nix": "nix",
121
+ ".typ": "typst",
122
+ ".typc": "typst",
123
+ }
124
+
125
+ export function filetype(input?: string) {
126
+ if (!input) return "none"
127
+ const language = LANGUAGE_EXTENSIONS[path.extname(input)]
128
+ if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
129
+ return language
130
+ }
@@ -0,0 +1,20 @@
1
+ export function formatDuration(secs: number) {
2
+ if (secs <= 0) return ""
3
+ if (secs < 60) return `${secs}s`
4
+ if (secs < 3600) {
5
+ const mins = Math.floor(secs / 60)
6
+ const remaining = secs % 60
7
+ return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m`
8
+ }
9
+ if (secs < 86400) {
10
+ const hours = Math.floor(secs / 3600)
11
+ const remaining = Math.floor((secs % 3600) / 60)
12
+ return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h`
13
+ }
14
+ if (secs < 604800) {
15
+ const days = Math.floor(secs / 86400)
16
+ return days === 1 ? "~1 day" : `~${days} days`
17
+ }
18
+ const weeks = Math.floor(secs / 604800)
19
+ return weeks === 1 ? "~1 week" : `~${weeks} weeks`
20
+ }
@@ -0,0 +1,25 @@
1
+ import type { BaseRenderable, BoxRenderable } from "@opentui/core"
2
+
3
+ const previousByParent = new WeakMap<
4
+ BaseRenderable,
5
+ { frameID: number; previous: WeakMap<BaseRenderable, BaseRenderable | undefined> }
6
+ >()
7
+
8
+ export function setPreLayoutSiblingMargin(el: BoxRenderable, margin: (previous?: BaseRenderable) => number) {
9
+ // Run before Yoga layout so scroll geometry matches the rendered frame.
10
+ el.onLifecyclePass = () => {
11
+ const parent = el.parent
12
+ if (!parent) return
13
+ const cached = previousByParent.get(parent)
14
+ const previous = cached?.frameID === el.ctx.frameId ? cached.previous : previousSiblings(parent, el.ctx.frameId)
15
+ const value = margin(previous.get(el))
16
+ if (el.marginTop !== value) el.marginTop = value
17
+ }
18
+ }
19
+
20
+ function previousSiblings(parent: BaseRenderable, frameID: number) {
21
+ const previous = new WeakMap<BaseRenderable, BaseRenderable | undefined>()
22
+ parent.getChildren().forEach((child, index, children) => previous.set(child, children[index - 1]))
23
+ previousByParent.set(parent, { frameID, previous })
24
+ return previous
25
+ }
@@ -0,0 +1,86 @@
1
+ export function titlecase(str: string) {
2
+ return str.replace(/\b\w/g, (c) => c.toUpperCase())
3
+ }
4
+
5
+ export function time(input: number): string {
6
+ const date = new Date(input)
7
+ return date.toLocaleTimeString(undefined, { timeStyle: "short" })
8
+ }
9
+
10
+ export function datetime(input: number): string {
11
+ const date = new Date(input)
12
+ const localTime = time(input)
13
+ const localDate = date.toLocaleDateString()
14
+ return `${localTime} · ${localDate}`
15
+ }
16
+
17
+ export function todayTimeOrDateTime(input: number): string {
18
+ const date = new Date(input)
19
+ const now = new Date()
20
+ const isToday =
21
+ date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
22
+
23
+ if (isToday) {
24
+ return time(input)
25
+ } else {
26
+ return datetime(input)
27
+ }
28
+ }
29
+
30
+ export function number(num: number): string {
31
+ if (num >= 1000000) {
32
+ return (num / 1000000).toFixed(1) + "M"
33
+ } else if (num >= 1000) {
34
+ return (num / 1000).toFixed(1) + "K"
35
+ }
36
+ return num.toString()
37
+ }
38
+
39
+ export function duration(input: number) {
40
+ if (input < 1000) {
41
+ return `${input}ms`
42
+ }
43
+ if (input < 60000) {
44
+ return `${(input / 1000).toFixed(1)}s`
45
+ }
46
+ if (input < 3600000) {
47
+ const minutes = Math.floor(input / 60000)
48
+ const seconds = Math.floor((input % 60000) / 1000)
49
+ return `${minutes}m ${seconds}s`
50
+ }
51
+ if (input < 86400000) {
52
+ const hours = Math.floor(input / 3600000)
53
+ const minutes = Math.floor((input % 3600000) / 60000)
54
+ return `${hours}h ${minutes}m`
55
+ }
56
+ const days = Math.floor(input / 86400000)
57
+ const hours = Math.floor((input % 86400000) / 3600000)
58
+ return `${days}d ${hours}h`
59
+ }
60
+
61
+ export function truncate(str: string, len: number): string {
62
+ if (str.length <= len) return str
63
+ return str.slice(0, len - 1) + "…"
64
+ }
65
+
66
+ export function truncateLeft(str: string, len: number): string {
67
+ if (str.length <= len) return str
68
+ return "…" + str.slice(-(len - 1))
69
+ }
70
+
71
+ export function truncateMiddle(str: string, maxLength: number = 35): string {
72
+ if (str.length <= maxLength) return str
73
+
74
+ const ellipsis = "…"
75
+ const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
76
+ const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
77
+
78
+ return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
79
+ }
80
+
81
+ export function pluralize(count: number, singular: string, plural: string): string {
82
+ const template = count === 1 ? singular : plural
83
+ return template.replace("{}", count.toString())
84
+ }
85
+
86
+ export * as Locale from "./locale"
@@ -0,0 +1,28 @@
1
+ import type { Provider } from "@neurocode-ai/sdk/v2"
2
+
3
+ export function parse(value: string) {
4
+ const [providerID, ...modelID] = value.split("/")
5
+ return { providerID, modelID: modelID.join("/") }
6
+ }
7
+
8
+ export function index(list: Provider[] | undefined) {
9
+ return new Map((list ?? []).map((item) => [item.id, item] as const))
10
+ }
11
+
12
+ export function get(list: Provider[] | ReadonlyMap<string, Provider> | undefined, providerID: string, modelID: string) {
13
+ const provider =
14
+ list instanceof Map
15
+ ? list.get(providerID)
16
+ : Array.isArray(list)
17
+ ? list.find((item) => item.id === providerID)
18
+ : undefined
19
+ return provider?.models[modelID]
20
+ }
21
+
22
+ export function name(
23
+ list: Provider[] | ReadonlyMap<string, Provider> | undefined,
24
+ providerID: string,
25
+ modelID: string,
26
+ ) {
27
+ return get(list, providerID, modelID)?.name ?? modelID
28
+ }
@@ -0,0 +1,12 @@
1
+ import { realpathSync } from "node:fs"
2
+ import { win32 } from "node:path"
3
+
4
+ export function normalizePath(input: string, platform: string) {
5
+ if (platform !== "win32") return input
6
+ const resolved = win32.normalize(win32.resolve(input.replaceAll("/", "\\")))
7
+ try {
8
+ return realpathSync.native(resolved)
9
+ } catch {
10
+ return resolved
11
+ }
12
+ }
@@ -0,0 +1,33 @@
1
+ import path from "path"
2
+ import { appendFile, mkdir, rename, rm } from "fs/promises"
3
+
4
+ export function readText(filePath: string) {
5
+ return Bun.file(filePath).text()
6
+ }
7
+
8
+ export function readJson<T>(filePath: string) {
9
+ return Bun.file(filePath).json() as Promise<T>
10
+ }
11
+
12
+ export async function writeText(filePath: string, content: string) {
13
+ await mkdir(path.dirname(filePath), { recursive: true })
14
+ await Bun.write(filePath, content)
15
+ }
16
+
17
+ export async function appendText(filePath: string, content: string) {
18
+ await mkdir(path.dirname(filePath), { recursive: true })
19
+ await appendFile(filePath, content)
20
+ }
21
+
22
+ export async function writeJsonAtomic(filePath: string, value: unknown) {
23
+ await mkdir(path.dirname(filePath), { recursive: true })
24
+ const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`
25
+ await Bun.write(temporary, JSON.stringify(value)).catch(async (error) => {
26
+ await rm(temporary, { force: true }).catch(() => undefined)
27
+ throw error
28
+ })
29
+ await rename(temporary, filePath).catch(async (error) => {
30
+ await rm(temporary, { force: true }).catch(() => undefined)
31
+ throw error
32
+ })
33
+ }
@@ -0,0 +1,38 @@
1
+ const logo = {
2
+ left: [" ", "█▀▀▄ █▀▀█ █▀▀█ █▀▀▄ █▀▀█", "█__█ █^^^ █__█ █▀▀▀ █__█", "▀~~▀ ▀▀▀▀ ▀▀▀▀ ▀__▀ ▀▀▀▀"],
3
+ right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀▄ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
4
+ }
5
+
6
+ const reset = "\x1b[0m"
7
+ const bold = "\x1b[1m"
8
+ const dim = "\x1b[90m"
9
+
10
+ function wordmark(pad = "") {
11
+ const draw = (line: string, fg: string, shadow: string, bg: string) =>
12
+ [...line]
13
+ .map((char) => {
14
+ if (char === "_") return `${bg} ${reset}`
15
+ if (char === "^") return `${fg}${bg}▀${reset}`
16
+ if (char === "~") return `${shadow}▀${reset}`
17
+ if (char === " ") return " "
18
+ return `${fg}${char}${reset}`
19
+ })
20
+ .join("")
21
+
22
+ return logo.left.map((line, index) => {
23
+ const left = draw(line, dim, "\x1b[38;5;235m", "\x1b[48;5;235m")
24
+ const right = draw(logo.right[index] ?? "", reset, "\x1b[38;5;238m", "\x1b[48;5;238m")
25
+ return `${pad}${left} ${right}`
26
+ })
27
+ }
28
+
29
+ export function sessionEpilogue(input: { title: string; sessionID?: string }) {
30
+ const weak = (text: string) => `${dim}${text.padEnd(10, " ")}${reset}`
31
+ return [
32
+ ...wordmark(" "),
33
+ "",
34
+ ` ${weak("Session")}${bold}${input.title}${reset}`,
35
+ ` ${weak("Continue")}${bold}neurocode -s ${input.sessionID}${reset}`,
36
+ "",
37
+ ].join("\n")
38
+ }