@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.
- package/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- package/tsconfig.json +10 -0
package/src/ui/toast.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/util/path.ts
ADDED
|
@@ -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
|
+
}
|