@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export type LocalFiles = Readonly<{
|
|
5
|
+
readText(path: string): Promise<string>
|
|
6
|
+
readBytes(path: string): Promise<Uint8Array>
|
|
7
|
+
mime(path: string): Promise<string>
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
export type LocalAttachment =
|
|
11
|
+
| Readonly<{ type: "text"; mime: "image/svg+xml"; content: string }>
|
|
12
|
+
| Readonly<{ type: "binary"; mime: string; content: Uint8Array }>
|
|
13
|
+
|
|
14
|
+
export function readLocalAttachment(file: string) {
|
|
15
|
+
return readLocalAttachmentWith(
|
|
16
|
+
{
|
|
17
|
+
readText: (value) => readFile(value, "utf8"),
|
|
18
|
+
readBytes: (value) => readFile(value),
|
|
19
|
+
mime: async (value) => mimeTypes[path.extname(value).toLowerCase()] ?? "application/octet-stream",
|
|
20
|
+
},
|
|
21
|
+
file,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const mimeTypes: Record<string, string> = {
|
|
26
|
+
".avif": "image/avif",
|
|
27
|
+
".gif": "image/gif",
|
|
28
|
+
".jpeg": "image/jpeg",
|
|
29
|
+
".jpg": "image/jpeg",
|
|
30
|
+
".pdf": "application/pdf",
|
|
31
|
+
".png": "image/png",
|
|
32
|
+
".svg": "image/svg+xml",
|
|
33
|
+
".webp": "image/webp",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readLocalAttachmentWith(files: LocalFiles, path: string): Promise<LocalAttachment | undefined> {
|
|
37
|
+
const mime = await files.mime(path).catch(() => undefined)
|
|
38
|
+
if (!mime) return
|
|
39
|
+
if (mime === "image/svg+xml") {
|
|
40
|
+
const content = await files.readText(path).catch(() => undefined)
|
|
41
|
+
if (!content) return
|
|
42
|
+
return { type: "text", mime, content }
|
|
43
|
+
}
|
|
44
|
+
if (!mime.startsWith("image/") && mime !== "application/pdf") return
|
|
45
|
+
const content = await files.readBytes(path).catch(() => undefined)
|
|
46
|
+
if (!content) return
|
|
47
|
+
return { type: "binary", mime, content }
|
|
48
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { useTuiPaths } from "../../context/runtime"
|
|
4
|
+
import { errorMessage } from "../../util/error"
|
|
5
|
+
import { useDialog } from "../../ui/dialog"
|
|
6
|
+
import { useSDK } from "../../context/sdk"
|
|
7
|
+
import { useSync } from "../../context/sync"
|
|
8
|
+
import { useToast } from "../../ui/toast"
|
|
9
|
+
import { DialogMoveSession, type MoveSessionSelection } from "../dialog-move-session"
|
|
10
|
+
import { DialogWorkspaceFileChanges } from "../dialog-workspace-file-changes"
|
|
11
|
+
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
|
12
|
+
import { useProject } from "../../context/project"
|
|
13
|
+
|
|
14
|
+
function moveReminderText(directory: string) {
|
|
15
|
+
return `<system-reminder>The user has changed the current working directory to "${directory}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.</system-reminder>`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function usePromptMove(input: { projectID: () => string | undefined; sessionID: () => string | undefined }) {
|
|
19
|
+
const dialog = useDialog()
|
|
20
|
+
const sdk = useSDK()
|
|
21
|
+
const sync = useSync()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
const homeDestination = useHomeSessionDestination()
|
|
24
|
+
const project = useProject()
|
|
25
|
+
const paths = useTuiPaths()
|
|
26
|
+
const [creating, setCreating] = createSignal(false)
|
|
27
|
+
const [creatingDots, setCreatingDots] = createSignal(3)
|
|
28
|
+
const [progress, setProgress] = createSignal<string>()
|
|
29
|
+
|
|
30
|
+
async function create(context?: string) {
|
|
31
|
+
const projectID = input.projectID()
|
|
32
|
+
if (!projectID) return
|
|
33
|
+
setCreating(true)
|
|
34
|
+
setProgress("Creating copy")
|
|
35
|
+
try {
|
|
36
|
+
const generated = await sdk.client.experimental.projectCopy.generateName(
|
|
37
|
+
{ projectID, context },
|
|
38
|
+
{ throwOnError: true },
|
|
39
|
+
)
|
|
40
|
+
const result = await sdk.client.v2.projectCopy.create(
|
|
41
|
+
{
|
|
42
|
+
projectID,
|
|
43
|
+
location: { directory: sdk.directory },
|
|
44
|
+
strategy: "git_worktree",
|
|
45
|
+
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
|
46
|
+
name: generated.data.name,
|
|
47
|
+
},
|
|
48
|
+
{ throwOnError: true },
|
|
49
|
+
)
|
|
50
|
+
const directory = result.data?.directory
|
|
51
|
+
if (!directory) throw new Error("No project copy directory returned")
|
|
52
|
+
|
|
53
|
+
// Call a location-based route to make sure it's bootstrapped
|
|
54
|
+
// before moving on
|
|
55
|
+
await sdk.client.path.get({ directory }, { throwOnError: true })
|
|
56
|
+
|
|
57
|
+
setProgress("Creating session")
|
|
58
|
+
return directory
|
|
59
|
+
} catch (err) {
|
|
60
|
+
homeDestination?.clear()
|
|
61
|
+
setProgress(undefined)
|
|
62
|
+
setCreating(false)
|
|
63
|
+
toast.show({ title: "Creating workspace failed", message: errorMessage(err), variant: "error" })
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function open() {
|
|
69
|
+
const projectID = input.projectID()
|
|
70
|
+
if (!projectID) return
|
|
71
|
+
const sessionID = input.sessionID()
|
|
72
|
+
const session = sessionID ? sync.session.get(sessionID) : undefined
|
|
73
|
+
dialog.replace(() => (
|
|
74
|
+
<DialogMoveSession
|
|
75
|
+
projectID={projectID}
|
|
76
|
+
current={
|
|
77
|
+
homeDestination?.destination() ??
|
|
78
|
+
(session
|
|
79
|
+
? {
|
|
80
|
+
type: "directory",
|
|
81
|
+
directory: session.directory,
|
|
82
|
+
subdirectory: !!session.path,
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
type: "directory",
|
|
86
|
+
directory: project.instance.directory(),
|
|
87
|
+
subdirectory: project.instance.directory() !== project.instance.path().worktree,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
onCurrentChange={(selection) => homeDestination?.setDestination(selection)}
|
|
91
|
+
onSelect={(selection) => {
|
|
92
|
+
const sessionID = input.sessionID()
|
|
93
|
+
if (!sessionID) {
|
|
94
|
+
homeDestination?.setDestination(selection)
|
|
95
|
+
dialog.clear()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
void moveExistingSession(sessionID, selection)
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sessionContext(sessionID: string) {
|
|
105
|
+
const session = sync.session.get(sessionID)
|
|
106
|
+
const messages = (sync.data.message[sessionID] ?? [])
|
|
107
|
+
.slice(-6)
|
|
108
|
+
.map((message) =>
|
|
109
|
+
[
|
|
110
|
+
message.role + ":",
|
|
111
|
+
...(sync.data.part[message.id] ?? []).flatMap((part) => (part.type === "text" ? [part.text] : [])),
|
|
112
|
+
].join(" "),
|
|
113
|
+
)
|
|
114
|
+
return [session?.title, ...messages].filter(Boolean).join("\n") || undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function moveExistingSession(sessionID: string, selection: MoveSessionSelection) {
|
|
118
|
+
const session = sync.session.get(sessionID)
|
|
119
|
+
const status = await sdk.client.vcs.status({ directory: session?.directory }).catch(() => undefined)
|
|
120
|
+
const choice = status?.data?.length ? await DialogWorkspaceFileChanges.show(dialog, status.data) : "no"
|
|
121
|
+
if (!choice) return
|
|
122
|
+
dialog.clear()
|
|
123
|
+
const directory = selection.type === "new" ? await create(sessionContext(sessionID)) : selection.directory
|
|
124
|
+
if (!directory) {
|
|
125
|
+
setProgress(undefined)
|
|
126
|
+
dialog.clear()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
setProgress("Moving session")
|
|
130
|
+
try {
|
|
131
|
+
await sdk.client.experimental.controlPlane.moveSession(
|
|
132
|
+
{
|
|
133
|
+
sessionID,
|
|
134
|
+
destination: { directory },
|
|
135
|
+
moveChanges: choice === "yes",
|
|
136
|
+
},
|
|
137
|
+
{ throwOnError: true },
|
|
138
|
+
)
|
|
139
|
+
await sdk.client.session
|
|
140
|
+
.promptAsync({
|
|
141
|
+
sessionID,
|
|
142
|
+
directory,
|
|
143
|
+
noReply: true,
|
|
144
|
+
parts: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: moveReminderText(directory),
|
|
148
|
+
synthetic: true,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
})
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
dialog.clear()
|
|
154
|
+
} catch (error) {
|
|
155
|
+
toast.error(error)
|
|
156
|
+
dialog.clear()
|
|
157
|
+
} finally {
|
|
158
|
+
setProgress(undefined)
|
|
159
|
+
setCreating(false)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pending = createMemo(() => Boolean(homeDestination?.destination()))
|
|
164
|
+
const pendingNew = createMemo(() => homeDestination?.destination()?.type === "new")
|
|
165
|
+
|
|
166
|
+
async function getDirectory(context?: string) {
|
|
167
|
+
const value = homeDestination?.destination()
|
|
168
|
+
if (!value) return
|
|
169
|
+
if (value.type === "directory") {
|
|
170
|
+
return value.directory
|
|
171
|
+
}
|
|
172
|
+
return await create(context)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function startSubmit() {
|
|
176
|
+
if (progress()) setProgress("Submitting prompt")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function finishSubmit() {
|
|
180
|
+
homeDestination?.clear()
|
|
181
|
+
setProgress(undefined)
|
|
182
|
+
setCreating(false)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
createEffect(() => {
|
|
186
|
+
if (!creating()) {
|
|
187
|
+
setCreatingDots(3)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const timer = setInterval(() => setCreatingDots((dots) => (dots % 3) + 1), 1000)
|
|
191
|
+
onCleanup(() => clearInterval(timer))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
creating,
|
|
196
|
+
creatingDots,
|
|
197
|
+
finishSubmit,
|
|
198
|
+
getDirectory,
|
|
199
|
+
open,
|
|
200
|
+
pending,
|
|
201
|
+
pendingNew,
|
|
202
|
+
progress,
|
|
203
|
+
startSubmit,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../prompt/stash"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
|
2
|
+
import { useDialog } from "../../ui/dialog"
|
|
3
|
+
import { useSDK } from "../../context/sdk"
|
|
4
|
+
import { useProject } from "../../context/project"
|
|
5
|
+
import { useSync } from "../../context/sync"
|
|
6
|
+
import { useToast } from "../../ui/toast"
|
|
7
|
+
import { errorMessage } from "../../util/error"
|
|
8
|
+
import {
|
|
9
|
+
confirmWorkspaceFileChanges,
|
|
10
|
+
openWorkspaceSelect,
|
|
11
|
+
warpWorkspaceSession,
|
|
12
|
+
type WorkspaceSelection,
|
|
13
|
+
} from "../dialog-workspace-create"
|
|
14
|
+
import type { WorkspaceStatus } from "../workspace-label"
|
|
15
|
+
|
|
16
|
+
export function usePromptWorkspace(sessionID?: string) {
|
|
17
|
+
const dialog = useDialog()
|
|
18
|
+
const sdk = useSDK()
|
|
19
|
+
const project = useProject()
|
|
20
|
+
const sync = useSync()
|
|
21
|
+
const toast = useToast()
|
|
22
|
+
const [selection, setSelection] = createSignal<WorkspaceSelection>()
|
|
23
|
+
const [creating, setCreating] = createSignal(false)
|
|
24
|
+
const [creatingDots, setCreatingDots] = createSignal(3)
|
|
25
|
+
const [notice, setNotice] = createSignal<string>()
|
|
26
|
+
|
|
27
|
+
async function create(selection: Extract<WorkspaceSelection, { type: "new" }>) {
|
|
28
|
+
setCreating(true)
|
|
29
|
+
let result
|
|
30
|
+
try {
|
|
31
|
+
result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setSelection(undefined)
|
|
34
|
+
setCreating(false)
|
|
35
|
+
toast.show({ title: "Creating workspace failed", message: errorMessage(err), variant: "error" })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (result.error || !result.data) {
|
|
39
|
+
setSelection(undefined)
|
|
40
|
+
setCreating(false)
|
|
41
|
+
toast.show({
|
|
42
|
+
title: "Creating workspace failed",
|
|
43
|
+
message: errorMessage(result.error ?? "no response"),
|
|
44
|
+
variant: "error",
|
|
45
|
+
})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await project.workspace.sync()
|
|
50
|
+
const workspace = result.data
|
|
51
|
+
setSelection({
|
|
52
|
+
type: "existing",
|
|
53
|
+
workspaceID: workspace.id,
|
|
54
|
+
workspaceType: workspace.type,
|
|
55
|
+
workspaceName: workspace.name,
|
|
56
|
+
})
|
|
57
|
+
setCreating(false)
|
|
58
|
+
return workspace
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function warp(selection: WorkspaceSelection) {
|
|
62
|
+
if (!sessionID) {
|
|
63
|
+
setSelection(selection)
|
|
64
|
+
dialog.clear()
|
|
65
|
+
if (selection.type === "new") void create(selection)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
const sourceWorkspaceID = project.workspace.current()
|
|
69
|
+
const copyChanges = await confirmWorkspaceFileChanges({ dialog, sdk, sourceWorkspaceID })
|
|
70
|
+
if (copyChanges === undefined) return
|
|
71
|
+
setSelection(selection)
|
|
72
|
+
dialog.clear()
|
|
73
|
+
|
|
74
|
+
const workspace =
|
|
75
|
+
selection.type === "none"
|
|
76
|
+
? { id: null, name: "local project" }
|
|
77
|
+
: selection.type === "existing"
|
|
78
|
+
? { id: selection.workspaceID, name: selection.workspaceName }
|
|
79
|
+
: await create(selection)
|
|
80
|
+
if (!workspace) return
|
|
81
|
+
|
|
82
|
+
const warped = await warpWorkspaceSession({
|
|
83
|
+
dialog,
|
|
84
|
+
sdk,
|
|
85
|
+
sync,
|
|
86
|
+
project,
|
|
87
|
+
toast,
|
|
88
|
+
sourceWorkspaceID,
|
|
89
|
+
workspaceID: workspace.id,
|
|
90
|
+
sessionID,
|
|
91
|
+
copyChanges,
|
|
92
|
+
})
|
|
93
|
+
if (warped) showNotice(workspace.name)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function showNotice(name: string) {
|
|
97
|
+
setNotice(`Warped to ${name}`)
|
|
98
|
+
setTimeout(() => setNotice(undefined), 4000)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clearNotice() {
|
|
102
|
+
setNotice(undefined)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function open() {
|
|
106
|
+
void openWorkspaceSelect({ dialog, sdk, sync, project, toast, onSelect: warp })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
createEffect(() => {
|
|
110
|
+
if (!creating()) {
|
|
111
|
+
setCreatingDots(3)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const timer = setInterval(() => setCreatingDots((dots) => (dots % 3) + 1), 1000)
|
|
115
|
+
onCleanup(() => clearInterval(timer))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const label = createMemo<
|
|
119
|
+
| { type: "new"; workspaceType: string }
|
|
120
|
+
| { type: "existing"; workspaceType: string; workspaceName: string; status?: WorkspaceStatus }
|
|
121
|
+
| undefined
|
|
122
|
+
>(() => {
|
|
123
|
+
const selected = selection()
|
|
124
|
+
if (!selected) return
|
|
125
|
+
if (selected.type === "none") return
|
|
126
|
+
if (sessionID && !creating()) return
|
|
127
|
+
if (selected.type === "new") return { type: "new", workspaceType: selected.workspaceType }
|
|
128
|
+
return {
|
|
129
|
+
type: "existing",
|
|
130
|
+
workspaceType: selected.workspaceType,
|
|
131
|
+
workspaceName: selected.workspaceName,
|
|
132
|
+
status: selected.type === "existing" ? "connected" : undefined,
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return { selection, creating, creatingDots, notice, label, open, warp, clearNotice }
|
|
137
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Show } from "solid-js"
|
|
2
|
+
import { useTheme } from "../context/theme"
|
|
3
|
+
import { useKV } from "../context/kv"
|
|
4
|
+
import type { JSX } from "@opentui/solid"
|
|
5
|
+
import type { RGBA } from "@opentui/core"
|
|
6
|
+
import { registerNeurocodeSpinner } from "./register-spinner"
|
|
7
|
+
|
|
8
|
+
registerNeurocodeSpinner()
|
|
9
|
+
|
|
10
|
+
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
11
|
+
|
|
12
|
+
export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
|
|
13
|
+
const { theme } = useTheme()
|
|
14
|
+
const kv = useKV()
|
|
15
|
+
const color = () => props.color ?? theme.textMuted
|
|
16
|
+
return (
|
|
17
|
+
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯ {props.children}</text>}>
|
|
18
|
+
<box flexDirection="row" gap={1}>
|
|
19
|
+
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
|
20
|
+
<Show when={props.children}>
|
|
21
|
+
<text fg={color()}>{props.children}</text>
|
|
22
|
+
</Show>
|
|
23
|
+
</box>
|
|
24
|
+
</Show>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js"
|
|
2
|
+
import { useTheme } from "../context/theme"
|
|
3
|
+
import { Spinner } from "./spinner"
|
|
4
|
+
|
|
5
|
+
export function StartupLoading(props: { ready: () => boolean }) {
|
|
6
|
+
const theme = useTheme().theme
|
|
7
|
+
const [show, setShow] = createSignal(false)
|
|
8
|
+
const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins..."))
|
|
9
|
+
let wait: NodeJS.Timeout | undefined
|
|
10
|
+
let hold: NodeJS.Timeout | undefined
|
|
11
|
+
let stamp = 0
|
|
12
|
+
|
|
13
|
+
createEffect(() => {
|
|
14
|
+
if (props.ready()) {
|
|
15
|
+
if (wait) {
|
|
16
|
+
clearTimeout(wait)
|
|
17
|
+
wait = undefined
|
|
18
|
+
}
|
|
19
|
+
if (!show()) return
|
|
20
|
+
if (hold) return
|
|
21
|
+
|
|
22
|
+
const left = 3000 - (Date.now() - stamp)
|
|
23
|
+
if (left <= 0) {
|
|
24
|
+
setShow(false)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
hold = setTimeout(() => {
|
|
29
|
+
hold = undefined
|
|
30
|
+
setShow(false)
|
|
31
|
+
}, left).unref()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (hold) {
|
|
36
|
+
clearTimeout(hold)
|
|
37
|
+
hold = undefined
|
|
38
|
+
}
|
|
39
|
+
if (show()) return
|
|
40
|
+
if (wait) return
|
|
41
|
+
|
|
42
|
+
wait = setTimeout(() => {
|
|
43
|
+
wait = undefined
|
|
44
|
+
stamp = Date.now()
|
|
45
|
+
setShow(true)
|
|
46
|
+
}, 500).unref()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
onCleanup(() => {
|
|
50
|
+
if (wait) clearTimeout(wait)
|
|
51
|
+
if (hold) clearTimeout(hold)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Show when={show()}>
|
|
56
|
+
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
|
|
57
|
+
<box backgroundColor={theme.backgroundPanel} paddingLeft={1} paddingRight={1}>
|
|
58
|
+
<Spinner color={theme.textMuted}>{text()}</Spinner>
|
|
59
|
+
</box>
|
|
60
|
+
</box>
|
|
61
|
+
</Show>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useTheme } from "../context/theme"
|
|
2
|
+
|
|
3
|
+
export interface TodoItemProps {
|
|
4
|
+
status: string
|
|
5
|
+
content: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function TodoItem(props: TodoItemProps) {
|
|
9
|
+
const { theme } = useTheme()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<box flexDirection="row" gap={0}>
|
|
13
|
+
<text
|
|
14
|
+
flexShrink={0}
|
|
15
|
+
style={{
|
|
16
|
+
fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
[{props.status === "completed" ? "✓" : props.status === "in_progress" ? "•" : " "}]{" "}
|
|
20
|
+
</text>
|
|
21
|
+
<text
|
|
22
|
+
flexGrow={1}
|
|
23
|
+
wrapMode="word"
|
|
24
|
+
style={{
|
|
25
|
+
fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{props.content}
|
|
29
|
+
</text>
|
|
30
|
+
</box>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createMemo } from "solid-js"
|
|
2
|
+
import { useSync } from "../context/sync"
|
|
3
|
+
|
|
4
|
+
export function useConnected() {
|
|
5
|
+
const sync = useSync()
|
|
6
|
+
return createMemo(() =>
|
|
7
|
+
sync.data.provider.some(
|
|
8
|
+
(provider) =>
|
|
9
|
+
provider.id !== "opencode" || Object.values(provider.models).some((model) => model.cost?.input !== 0),
|
|
10
|
+
),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useTheme } from "../context/theme"
|
|
2
|
+
|
|
3
|
+
export type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
|
4
|
+
|
|
5
|
+
export function WorkspaceLabel(props: { type: string; name: string; status?: WorkspaceStatus; icon?: boolean }) {
|
|
6
|
+
const { theme } = useTheme()
|
|
7
|
+
const color = () => {
|
|
8
|
+
if (props.status === "connected") return theme.success
|
|
9
|
+
if (props.status === "error") return theme.error
|
|
10
|
+
return theme.textMuted
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
{props.icon ? <span style={{ fg: color() }}>● </span> : undefined}
|
|
16
|
+
<span style={{ fg: theme.text }}>{props.name}</span> <span style={{ fg: theme.textMuted }}>({props.type})</span>
|
|
17
|
+
</>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export * as TuiConfig from "."
|
|
2
|
+
|
|
3
|
+
import { createBindingLookup } from "@opentui/keymap/extras"
|
|
4
|
+
import { Schema } from "effect"
|
|
5
|
+
import { createContext, type JSX, useContext } from "solid-js"
|
|
6
|
+
import { TuiKeybind } from "./keybind"
|
|
7
|
+
|
|
8
|
+
export const AttentionSoundName = Schema.Literals([
|
|
9
|
+
"default",
|
|
10
|
+
"question",
|
|
11
|
+
"permission",
|
|
12
|
+
"error",
|
|
13
|
+
"done",
|
|
14
|
+
"subagent_done",
|
|
15
|
+
])
|
|
16
|
+
export type AttentionSoundName = Schema.Schema.Type<typeof AttentionSoundName>
|
|
17
|
+
|
|
18
|
+
export const PluginOptions = Schema.Record(Schema.String, Schema.Unknown)
|
|
19
|
+
export const PluginSpec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, PluginOptions]))])
|
|
20
|
+
|
|
21
|
+
export const LeaderTimeoutDefault = 2000
|
|
22
|
+
export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
|
|
23
|
+
description: "Leader key timeout in milliseconds",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const ScrollSpeed = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))
|
|
27
|
+
export const ScrollAcceleration = Schema.Struct({
|
|
28
|
+
enabled: Schema.Boolean.annotate({ description: "Enable scroll acceleration" }),
|
|
29
|
+
}).annotate({ description: "Scroll acceleration settings" })
|
|
30
|
+
export const DiffStyle = Schema.Literals(["auto", "stacked"]).annotate({
|
|
31
|
+
description: "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const AttentionSounds = Schema.Record(AttentionSoundName, Schema.optionalKey(Schema.String))
|
|
35
|
+
export type AttentionSoundPaths = Schema.Schema.Type<typeof AttentionSounds>
|
|
36
|
+
export const Attention = Schema.Struct({
|
|
37
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
38
|
+
notifications: Schema.optional(Schema.Boolean),
|
|
39
|
+
sound: Schema.optional(Schema.Boolean),
|
|
40
|
+
volume: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1))),
|
|
41
|
+
sound_pack: Schema.optional(Schema.String),
|
|
42
|
+
sounds: Schema.optional(AttentionSounds),
|
|
43
|
+
}).annotate({ description: "Attention notification and sound settings" })
|
|
44
|
+
|
|
45
|
+
const PromptSize = Schema.Int.check(Schema.isGreaterThan(0))
|
|
46
|
+
export const Prompt = Schema.Struct({
|
|
47
|
+
max_height: Schema.optional(PromptSize).annotate({ description: "Prompt textarea max height" }),
|
|
48
|
+
max_width: Schema.optional(Schema.Union([PromptSize, Schema.Literal("auto")])).annotate({
|
|
49
|
+
description: "Home prompt max width: a positive integer for a fixed cap, or 'auto' to scale with terminal width",
|
|
50
|
+
}),
|
|
51
|
+
}).annotate({ description: "Prompt size settings" })
|
|
52
|
+
|
|
53
|
+
export const Info = Schema.Struct({
|
|
54
|
+
$schema: Schema.optional(Schema.String),
|
|
55
|
+
theme: Schema.optional(Schema.String),
|
|
56
|
+
keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
|
|
57
|
+
plugin: Schema.optional(Schema.Array(PluginSpec)),
|
|
58
|
+
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
|
59
|
+
leader_timeout: Schema.optional(LeaderTimeout),
|
|
60
|
+
attention: Schema.optional(Attention),
|
|
61
|
+
prompt: Schema.optional(Prompt),
|
|
62
|
+
scroll_speed: Schema.optional(ScrollSpeed).annotate({ description: "TUI scroll speed" }),
|
|
63
|
+
scroll_acceleration: Schema.optional(ScrollAcceleration),
|
|
64
|
+
diff_style: Schema.optional(DiffStyle),
|
|
65
|
+
mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }),
|
|
66
|
+
})
|
|
67
|
+
export type Info = Schema.Schema.Type<typeof Info>
|
|
68
|
+
|
|
69
|
+
export type Resolved = Omit<Info, "attention" | "keybinds" | "leader_timeout" | "mouse"> & {
|
|
70
|
+
attention: {
|
|
71
|
+
enabled: boolean
|
|
72
|
+
notifications: boolean
|
|
73
|
+
sound: boolean
|
|
74
|
+
volume: number
|
|
75
|
+
sound_pack: string
|
|
76
|
+
sounds: AttentionSoundPaths
|
|
77
|
+
}
|
|
78
|
+
keybinds: TuiKeybind.BindingLookupView
|
|
79
|
+
leader_timeout: number
|
|
80
|
+
mouse: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const ResolveOptions = Schema.Struct({
|
|
84
|
+
terminalSuspend: Schema.Boolean,
|
|
85
|
+
})
|
|
86
|
+
export type ResolveOptions = Schema.Schema.Type<typeof ResolveOptions>
|
|
87
|
+
|
|
88
|
+
export function resolve(input: Info, options: ResolveOptions): Resolved {
|
|
89
|
+
const keybinds: TuiKeybind.KeybindOverrides = { ...input.keybinds }
|
|
90
|
+
if (!options.terminalSuspend) {
|
|
91
|
+
keybinds.terminal_suspend = "none"
|
|
92
|
+
if (keybinds.input_undo === undefined) {
|
|
93
|
+
const inputUndo = TuiKeybind.defaultValue("input_undo")
|
|
94
|
+
keybinds.input_undo = ["ctrl+z", ...(typeof inputUndo === "string" ? inputUndo.split(",") : [])]
|
|
95
|
+
.filter((value, index, values) => values.indexOf(value) === index)
|
|
96
|
+
.join(",")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...input,
|
|
102
|
+
attention: {
|
|
103
|
+
enabled: input.attention?.enabled ?? false,
|
|
104
|
+
notifications: input.attention?.notifications ?? true,
|
|
105
|
+
sound: input.attention?.sound ?? true,
|
|
106
|
+
volume: input.attention?.volume ?? 0.4,
|
|
107
|
+
sound_pack: input.attention?.sound_pack ?? "opencode.default",
|
|
108
|
+
sounds: input.attention?.sounds ?? {},
|
|
109
|
+
},
|
|
110
|
+
keybinds: createBindingLookup(TuiKeybind.toBindingConfig(TuiKeybind.parse(keybinds)), {
|
|
111
|
+
commandMap: TuiKeybind.CommandMap,
|
|
112
|
+
bindingDefaults: TuiKeybind.bindingDefaults(),
|
|
113
|
+
}),
|
|
114
|
+
leader_timeout: input.leader_timeout ?? LeaderTimeoutDefault,
|
|
115
|
+
mouse: input.mouse ?? true,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ConfigContext = createContext<Resolved>()
|
|
120
|
+
|
|
121
|
+
export function TuiConfigProvider(props: { config: Resolved; children: JSX.Element }) {
|
|
122
|
+
return <ConfigContext.Provider value={props.config}>{props.children}</ConfigContext.Provider>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useTuiConfig() {
|
|
126
|
+
const value = useContext(ConfigContext)
|
|
127
|
+
if (!value) throw new Error("TuiConfigProvider is missing")
|
|
128
|
+
return value
|
|
129
|
+
}
|