@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,144 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
import type { VcsFileStatus } from "@neurocode-ai/sdk/v2"
|
|
4
|
+
import { createMemo, For } from "solid-js"
|
|
5
|
+
import { createStore } from "solid-js/store"
|
|
6
|
+
import { Locale } from "../util/locale"
|
|
7
|
+
import { useTheme } from "../context/theme"
|
|
8
|
+
import { useTuiConfig } from "../config"
|
|
9
|
+
import { useDialog, type DialogContext } from "../ui/dialog"
|
|
10
|
+
import { getScrollAcceleration } from "../util/scroll"
|
|
11
|
+
|
|
12
|
+
const options = ["no", "yes"] as const
|
|
13
|
+
|
|
14
|
+
export type WorkspaceFileChangesChoice = (typeof options)[number]
|
|
15
|
+
|
|
16
|
+
function statusLabel(status: VcsFileStatus["status"]) {
|
|
17
|
+
if (status === "added") return "A"
|
|
18
|
+
if (status === "deleted") return "D"
|
|
19
|
+
return "M"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function changeCountWidth(file: VcsFileStatus) {
|
|
23
|
+
// The "plus 2" is for spaces
|
|
24
|
+
return `${file.additions ? `+${file.additions}` : ""}${file.deletions ? ` -${file.deletions}` : ""}`.length + 2
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DialogWorkspaceFileChanges(props: {
|
|
28
|
+
files: VcsFileStatus[]
|
|
29
|
+
onSelect: (choice: WorkspaceFileChangesChoice) => void
|
|
30
|
+
title?: string
|
|
31
|
+
message?: string
|
|
32
|
+
}) {
|
|
33
|
+
const dialog = useDialog()
|
|
34
|
+
const { theme } = useTheme()
|
|
35
|
+
const tuiConfig = useTuiConfig()
|
|
36
|
+
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
|
37
|
+
const [store, setStore] = createStore({ active: "yes" as WorkspaceFileChangesChoice })
|
|
38
|
+
const height = createMemo(() => Math.min(props.files.length, 8))
|
|
39
|
+
const fileNameWidth = createMemo(() => 48 - Math.max(Math.max(7, ...props.files.map(changeCountWidth)) - 7, 0))
|
|
40
|
+
|
|
41
|
+
function confirm() {
|
|
42
|
+
props.onSelect(store.active)
|
|
43
|
+
dialog.clear()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
useKeyboard((evt) => {
|
|
47
|
+
if (evt.name === "return") {
|
|
48
|
+
evt.preventDefault()
|
|
49
|
+
evt.stopPropagation()
|
|
50
|
+
confirm()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (evt.name === "left") {
|
|
54
|
+
evt.preventDefault()
|
|
55
|
+
evt.stopPropagation()
|
|
56
|
+
const index = options.indexOf(store.active)
|
|
57
|
+
setStore("active", options[Math.max(index - 1, 0)])
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (evt.name === "right") {
|
|
61
|
+
evt.preventDefault()
|
|
62
|
+
evt.stopPropagation()
|
|
63
|
+
const index = options.indexOf(store.active)
|
|
64
|
+
setStore("active", options[Math.min(index + 1, options.length - 1)])
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<box gap={1}>
|
|
70
|
+
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
|
71
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
72
|
+
{props.title ?? "File Changes Found"}
|
|
73
|
+
</text>
|
|
74
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
75
|
+
esc
|
|
76
|
+
</text>
|
|
77
|
+
</box>
|
|
78
|
+
<box paddingLeft={2} paddingRight={2}>
|
|
79
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
80
|
+
{props.message ?? "Do you want to move these changes with the session?"}
|
|
81
|
+
</text>
|
|
82
|
+
</box>
|
|
83
|
+
<scrollbox
|
|
84
|
+
height={height()}
|
|
85
|
+
backgroundColor={theme.backgroundElement}
|
|
86
|
+
scrollbarOptions={{ visible: false }}
|
|
87
|
+
scrollAcceleration={scrollAcceleration()}
|
|
88
|
+
>
|
|
89
|
+
<For each={props.files}>
|
|
90
|
+
{(item) => (
|
|
91
|
+
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
|
92
|
+
<box flexDirection="row" minWidth={0} flexShrink={1}>
|
|
93
|
+
<box width={2} flexShrink={0}>
|
|
94
|
+
<text fg={theme.textMuted}>{statusLabel(item.status)}</text>
|
|
95
|
+
</box>
|
|
96
|
+
<text fg={theme.textMuted} wrapMode="none">
|
|
97
|
+
{Locale.truncateLeft(item.file, fileNameWidth())}
|
|
98
|
+
</text>
|
|
99
|
+
</box>
|
|
100
|
+
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
|
|
101
|
+
<text>
|
|
102
|
+
{" "}
|
|
103
|
+
{item.additions ? <span style={{ fg: theme.diffAdded }}>+{item.additions}</span> : null}
|
|
104
|
+
{item.deletions ? <span style={{ fg: theme.diffRemoved }}> -{item.deletions}</span> : null}
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
</box>
|
|
108
|
+
)}
|
|
109
|
+
</For>
|
|
110
|
+
</scrollbox>
|
|
111
|
+
<box flexDirection="row" justifyContent="flex-end" paddingLeft={2} paddingRight={2} paddingBottom={1}>
|
|
112
|
+
<For each={options}>
|
|
113
|
+
{(item) => (
|
|
114
|
+
<box
|
|
115
|
+
paddingLeft={2}
|
|
116
|
+
paddingRight={2}
|
|
117
|
+
backgroundColor={item === store.active ? theme.primary : undefined}
|
|
118
|
+
onMouseUp={() => {
|
|
119
|
+
setStore("active", item)
|
|
120
|
+
props.onSelect(item)
|
|
121
|
+
dialog.clear()
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
|
|
125
|
+
</box>
|
|
126
|
+
)}
|
|
127
|
+
</For>
|
|
128
|
+
</box>
|
|
129
|
+
</box>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
DialogWorkspaceFileChanges.show = (
|
|
134
|
+
dialog: DialogContext,
|
|
135
|
+
files: VcsFileStatus[],
|
|
136
|
+
options?: { title?: string; message?: string },
|
|
137
|
+
) => {
|
|
138
|
+
return new Promise<WorkspaceFileChangesChoice | undefined>((resolve) => {
|
|
139
|
+
dialog.replace(
|
|
140
|
+
() => <DialogWorkspaceFileChanges files={files} onSelect={resolve} {...options} />,
|
|
141
|
+
() => resolve(undefined),
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Workspace } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
import { useDialog } from "../ui/dialog"
|
|
3
|
+
import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
|
|
4
|
+
import { useProject } from "../context/project"
|
|
5
|
+
import { useRoute } from "../context/route"
|
|
6
|
+
import { useSync } from "../context/sync"
|
|
7
|
+
import { useTheme } from "../context/theme"
|
|
8
|
+
import { createMemo, createSignal, onMount } from "solid-js"
|
|
9
|
+
import { createStore } from "solid-js/store"
|
|
10
|
+
import { errorMessage } from "../util/error"
|
|
11
|
+
import { useSDK } from "../context/sdk"
|
|
12
|
+
import { useToast } from "../ui/toast"
|
|
13
|
+
|
|
14
|
+
type WorkspaceOption = { workspace: Workspace }
|
|
15
|
+
|
|
16
|
+
export function DialogWorkspaceList() {
|
|
17
|
+
const dialog = useDialog()
|
|
18
|
+
const route = useRoute()
|
|
19
|
+
const sync = useSync()
|
|
20
|
+
const sdk = useSDK()
|
|
21
|
+
const toast = useToast()
|
|
22
|
+
const project = useProject()
|
|
23
|
+
const { theme } = useTheme()
|
|
24
|
+
const [deleting, setDeleting] = createSignal<string>()
|
|
25
|
+
const [removing, setRemoving] = createSignal<string>()
|
|
26
|
+
const [expanded, setExpanded] = createStore<Record<string, boolean>>({})
|
|
27
|
+
|
|
28
|
+
const current = createMemo(() => {
|
|
29
|
+
if (route.data.type === "session") return sync.session.get(route.data.sessionID)?.workspaceID
|
|
30
|
+
return project.workspace.current()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const options = createMemo<DialogSelectOption<WorkspaceOption>[]>(() =>
|
|
34
|
+
project.workspace
|
|
35
|
+
.list()
|
|
36
|
+
.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
37
|
+
.map((workspace) => {
|
|
38
|
+
const status = project.workspace.status(workspace.id)
|
|
39
|
+
return {
|
|
40
|
+
title:
|
|
41
|
+
removing() === workspace.id
|
|
42
|
+
? "Deleting..."
|
|
43
|
+
: deleting() === workspace.id
|
|
44
|
+
? `Delete ${workspace.name}? Press delete again`
|
|
45
|
+
: workspace.name,
|
|
46
|
+
value: { workspace },
|
|
47
|
+
footer: workspace.type,
|
|
48
|
+
details: expanded[workspace.id] && workspace.directory ? [workspace.directory] : undefined,
|
|
49
|
+
gutter: () => <text fg={status === "connected" ? theme.success : theme.error}>●</text>,
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
function showDetails(workspace: Workspace) {
|
|
55
|
+
setExpanded(workspace.id, (open) => !open)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function remove(workspace: Workspace) {
|
|
59
|
+
if (removing()) return
|
|
60
|
+
if (deleting() !== workspace.id) {
|
|
61
|
+
setDeleting(workspace.id)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setDeleting(undefined)
|
|
66
|
+
setRemoving(workspace.id)
|
|
67
|
+
const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({
|
|
68
|
+
error: err,
|
|
69
|
+
}))
|
|
70
|
+
if (result?.error) {
|
|
71
|
+
setRemoving(undefined)
|
|
72
|
+
toast.show({
|
|
73
|
+
variant: "error",
|
|
74
|
+
title: "Failed to delete workspace",
|
|
75
|
+
message: errorMessage(result.error),
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (current() === workspace.id) {
|
|
81
|
+
project.workspace.set(undefined)
|
|
82
|
+
route.navigate({ type: "home" })
|
|
83
|
+
}
|
|
84
|
+
await project.workspace.sync()
|
|
85
|
+
await sync.bootstrap({ fatal: false }).catch(() => undefined)
|
|
86
|
+
setRemoving(undefined)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onMount(() => {
|
|
90
|
+
dialog.setSize("large")
|
|
91
|
+
void sdk.client.experimental.workspace.syncList().catch(() => undefined)
|
|
92
|
+
void project.workspace.sync()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<DialogSelect
|
|
97
|
+
title="Workspaces"
|
|
98
|
+
options={options()}
|
|
99
|
+
onMove={(option) => {
|
|
100
|
+
setDeleting(undefined)
|
|
101
|
+
}}
|
|
102
|
+
onSelect={(option) => showDetails(option.value.workspace)}
|
|
103
|
+
actions={[
|
|
104
|
+
{
|
|
105
|
+
command: "session.delete",
|
|
106
|
+
title: "delete",
|
|
107
|
+
onTrigger: (option) => void remove(option.value.workspace),
|
|
108
|
+
},
|
|
109
|
+
]}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { For } from "solid-js"
|
|
4
|
+
import { useTheme } from "../context/theme"
|
|
5
|
+
import { useDialog } from "../ui/dialog"
|
|
6
|
+
import { useBindings } from "../keymap"
|
|
7
|
+
|
|
8
|
+
export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean | void | Promise<boolean | void> }) {
|
|
9
|
+
const dialog = useDialog()
|
|
10
|
+
const { theme } = useTheme()
|
|
11
|
+
const [store, setStore] = createStore({
|
|
12
|
+
active: "restore" as "cancel" | "restore",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const options = ["cancel", "restore"] as const
|
|
16
|
+
|
|
17
|
+
async function confirm() {
|
|
18
|
+
if (store.active === "cancel") {
|
|
19
|
+
dialog.clear()
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const result = await props.onRestore?.()
|
|
23
|
+
if (result === false) return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
useBindings(() => ({
|
|
27
|
+
bindings: [
|
|
28
|
+
{ key: "return", desc: "Confirm workspace option", group: "Dialog", cmd: () => void confirm() },
|
|
29
|
+
{ key: "left", desc: "Cancel workspace restore", group: "Dialog", cmd: () => setStore("active", "cancel") },
|
|
30
|
+
{ key: "right", desc: "Restore workspace", group: "Dialog", cmd: () => setStore("active", "restore") },
|
|
31
|
+
],
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
36
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
37
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
38
|
+
Workspace Unavailable
|
|
39
|
+
</text>
|
|
40
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
41
|
+
esc
|
|
42
|
+
</text>
|
|
43
|
+
</box>
|
|
44
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
45
|
+
This session is attached to a workspace that is no longer available.
|
|
46
|
+
</text>
|
|
47
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
48
|
+
Would you like to restore this session into a new workspace?
|
|
49
|
+
</text>
|
|
50
|
+
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
|
|
51
|
+
<For each={options}>
|
|
52
|
+
{(item) => (
|
|
53
|
+
<box
|
|
54
|
+
paddingLeft={2}
|
|
55
|
+
paddingRight={2}
|
|
56
|
+
backgroundColor={item === store.active ? theme.primary : undefined}
|
|
57
|
+
onMouseUp={() => {
|
|
58
|
+
setStore("active", item)
|
|
59
|
+
void confirm()
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
|
|
63
|
+
</box>
|
|
64
|
+
)}
|
|
65
|
+
</For>
|
|
66
|
+
</box>
|
|
67
|
+
</box>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
3
|
+
import { createSignal, For, Show } from "solid-js"
|
|
4
|
+
import { getScrollAcceleration } from "../util/scroll"
|
|
5
|
+
import { useClipboard } from "../context/clipboard"
|
|
6
|
+
import { InstallationVersion } from "@neurocode-ai/core/installation/version"
|
|
7
|
+
import { useExit } from "../context/exit"
|
|
8
|
+
import { describeOS, describeTerminal } from "../util/system"
|
|
9
|
+
|
|
10
|
+
export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) {
|
|
11
|
+
const term = useTerminalDimensions()
|
|
12
|
+
const exit = useExit()
|
|
13
|
+
const clipboard = useClipboard()
|
|
14
|
+
const [copied, setCopied] = createSignal(false)
|
|
15
|
+
|
|
16
|
+
// Safe fallback palette per mode (mirrors theme/assets/opencode.json) since the
|
|
17
|
+
// theme context may be the thing that crashed.
|
|
18
|
+
const isLight = props.mode === "light"
|
|
19
|
+
const colors = isLight
|
|
20
|
+
? {
|
|
21
|
+
bg: "#ffffff",
|
|
22
|
+
element: "#f5f5f5",
|
|
23
|
+
borderSubtle: "#d4d4d4",
|
|
24
|
+
text: "#1a1a1a",
|
|
25
|
+
muted: "#8a8a8a",
|
|
26
|
+
primary: "#3b7dd8",
|
|
27
|
+
onPrimary: "#ffffff",
|
|
28
|
+
error: "#d1383d",
|
|
29
|
+
success: "#3d9a57",
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
bg: "#0a0a0a",
|
|
33
|
+
element: "#1e1e1e",
|
|
34
|
+
borderSubtle: "#3c3c3c",
|
|
35
|
+
text: "#eeeeee",
|
|
36
|
+
muted: "#808080",
|
|
37
|
+
primary: "#fab283",
|
|
38
|
+
onPrimary: "#0a0a0a",
|
|
39
|
+
error: "#e06c75",
|
|
40
|
+
success: "#7fd88f",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const message = props.error.message || "An unknown error occurred."
|
|
44
|
+
const stack = props.error.stack || "No stack trace available."
|
|
45
|
+
const issueURL = buildIssueURL(message, stack)
|
|
46
|
+
|
|
47
|
+
const copyReport = () => {
|
|
48
|
+
void clipboard.write?.(issueURL.toString()).then(() => setCopied(true))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const actions = [
|
|
52
|
+
{ key: "c", label: () => (copied() ? "✓ Copied" : "Copy report"), copy: true, onUse: copyReport },
|
|
53
|
+
{ key: "r", label: () => "Restart", onUse: props.reset },
|
|
54
|
+
{ key: "q", label: () => "Quit", onUse: () => exit() },
|
|
55
|
+
]
|
|
56
|
+
const [selected, setSelected] = createSignal(0)
|
|
57
|
+
const move = (delta: number) => setSelected((prev) => (prev + delta + actions.length) % actions.length)
|
|
58
|
+
let scroll: ScrollBoxRenderable | undefined
|
|
59
|
+
|
|
60
|
+
useKeyboard((evt) => {
|
|
61
|
+
if (evt.ctrl && evt.name === "c") return exit()
|
|
62
|
+
if (evt.name === "return") {
|
|
63
|
+
evt.preventDefault()
|
|
64
|
+
evt.stopPropagation()
|
|
65
|
+
return actions[selected()].onUse()
|
|
66
|
+
}
|
|
67
|
+
if (evt.name === "left") {
|
|
68
|
+
evt.preventDefault()
|
|
69
|
+
evt.stopPropagation()
|
|
70
|
+
return move(-1)
|
|
71
|
+
}
|
|
72
|
+
if (evt.name === "right") {
|
|
73
|
+
evt.preventDefault()
|
|
74
|
+
evt.stopPropagation()
|
|
75
|
+
return move(1)
|
|
76
|
+
}
|
|
77
|
+
if (evt.name === "tab") {
|
|
78
|
+
evt.preventDefault()
|
|
79
|
+
evt.stopPropagation()
|
|
80
|
+
return move(evt.shift ? -1 : 1)
|
|
81
|
+
}
|
|
82
|
+
// Vertical keys scroll the stack trace; buttons navigate horizontally.
|
|
83
|
+
if (evt.name === "up") return scroll?.scrollBy(-1)
|
|
84
|
+
if (evt.name === "down") return scroll?.scrollBy(1)
|
|
85
|
+
if (evt.name === "pageup" && scroll) return scroll.scrollBy(-scroll.height)
|
|
86
|
+
if (evt.name === "pagedown" && scroll) return scroll.scrollBy(scroll.height)
|
|
87
|
+
if (evt.name === "home" && scroll) return scroll.scrollTo(0)
|
|
88
|
+
if (evt.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight)
|
|
89
|
+
if (evt.name === "q") return exit()
|
|
90
|
+
if (evt.name === "c") return copyReport()
|
|
91
|
+
if (evt.name === "r") return props.reset()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Responsive thresholds.
|
|
95
|
+
const contentWidth = () => Math.min(84, Math.max(24, term().width - 4))
|
|
96
|
+
const showSubtext = () => term().height >= 18
|
|
97
|
+
const showFooter = () => term().height >= 20
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<box
|
|
101
|
+
width={term().width}
|
|
102
|
+
height={term().height}
|
|
103
|
+
backgroundColor={colors.bg}
|
|
104
|
+
flexDirection="column"
|
|
105
|
+
alignItems="center"
|
|
106
|
+
>
|
|
107
|
+
<box width={contentWidth()} flexGrow={1} flexDirection="column" paddingTop={1} paddingBottom={1} gap={1}>
|
|
108
|
+
{/* Headline */}
|
|
109
|
+
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
|
110
|
+
<text attributes={TextAttributes.BOLD} fg={colors.text}>
|
|
111
|
+
opencode crashed
|
|
112
|
+
</text>
|
|
113
|
+
<Show when={showSubtext()}>
|
|
114
|
+
<text fg={colors.muted}>An unexpected error stopped the session.</text>
|
|
115
|
+
</Show>
|
|
116
|
+
</box>
|
|
117
|
+
|
|
118
|
+
{/* Error message panel */}
|
|
119
|
+
<box
|
|
120
|
+
flexShrink={0}
|
|
121
|
+
border
|
|
122
|
+
borderStyle="rounded"
|
|
123
|
+
borderColor={colors.error}
|
|
124
|
+
title=" Error "
|
|
125
|
+
titleColor={colors.error}
|
|
126
|
+
paddingLeft={2}
|
|
127
|
+
paddingRight={2}
|
|
128
|
+
>
|
|
129
|
+
<text fg={colors.text}>{message}</text>
|
|
130
|
+
</box>
|
|
131
|
+
|
|
132
|
+
{/* Actions */}
|
|
133
|
+
<box flexDirection="row" flexWrap="wrap" justifyContent="center" gap={2} rowGap={1} flexShrink={0}>
|
|
134
|
+
<For each={actions}>
|
|
135
|
+
{(action, index) => {
|
|
136
|
+
const isSelected = () => selected() === index()
|
|
137
|
+
const isCopied = () => action.copy && copied()
|
|
138
|
+
return (
|
|
139
|
+
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
|
140
|
+
<box
|
|
141
|
+
onMouseDown={() => setSelected(index())}
|
|
142
|
+
onMouseUp={() => action.onUse()}
|
|
143
|
+
backgroundColor={isCopied() ? colors.success : isSelected() ? colors.primary : colors.element}
|
|
144
|
+
minWidth={15}
|
|
145
|
+
alignItems="center"
|
|
146
|
+
paddingLeft={2}
|
|
147
|
+
paddingRight={2}
|
|
148
|
+
>
|
|
149
|
+
<text
|
|
150
|
+
attributes={TextAttributes.BOLD}
|
|
151
|
+
fg={isCopied() || isSelected() ? colors.onPrimary : colors.text}
|
|
152
|
+
>
|
|
153
|
+
{action.label()}
|
|
154
|
+
</text>
|
|
155
|
+
</box>
|
|
156
|
+
<text fg={isSelected() ? colors.primary : colors.muted}>{action.key}</text>
|
|
157
|
+
</box>
|
|
158
|
+
)
|
|
159
|
+
}}
|
|
160
|
+
</For>
|
|
161
|
+
</box>
|
|
162
|
+
|
|
163
|
+
{/* Stack trace */}
|
|
164
|
+
<box
|
|
165
|
+
flexGrow={1}
|
|
166
|
+
flexBasis={0}
|
|
167
|
+
minHeight={3}
|
|
168
|
+
border
|
|
169
|
+
borderStyle="rounded"
|
|
170
|
+
borderColor={colors.borderSubtle}
|
|
171
|
+
title=" Stack trace "
|
|
172
|
+
titleColor={colors.muted}
|
|
173
|
+
bottomTitle=" ↑↓ scroll "
|
|
174
|
+
bottomTitleAlignment="right"
|
|
175
|
+
paddingLeft={1}
|
|
176
|
+
paddingRight={1}
|
|
177
|
+
>
|
|
178
|
+
<scrollbox
|
|
179
|
+
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
|
180
|
+
flexGrow={1}
|
|
181
|
+
scrollAcceleration={getScrollAcceleration()}
|
|
182
|
+
>
|
|
183
|
+
<text fg={colors.muted}>{stack}</text>
|
|
184
|
+
</scrollbox>
|
|
185
|
+
</box>
|
|
186
|
+
|
|
187
|
+
{/* Footer */}
|
|
188
|
+
<Show when={showFooter()}>
|
|
189
|
+
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
|
190
|
+
<text fg={colors.muted}>
|
|
191
|
+
{copied()
|
|
192
|
+
? "Report copied — paste it into a new GitHub issue."
|
|
193
|
+
: "Copy the report and open a GitHub issue to help us fix this."}
|
|
194
|
+
</text>
|
|
195
|
+
<text fg={colors.muted}>opencode {InstallationVersion}</text>
|
|
196
|
+
</box>
|
|
197
|
+
</Show>
|
|
198
|
+
</box>
|
|
199
|
+
</box>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildIssueURL(message: string, stack: string) {
|
|
204
|
+
// Field keys match the ids in .github/ISSUE_TEMPLATE/bug-report.yml so the issue
|
|
205
|
+
// form opens pre-filled. Populating os/terminal/reproduce keeps the report past
|
|
206
|
+
// the contributing-guidelines compliance check, which pushes for system info.
|
|
207
|
+
const url = new URL("https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml")
|
|
208
|
+
url.searchParams.set("title", `TUI crash: ${message}`)
|
|
209
|
+
url.searchParams.set("opencode-version", InstallationVersion)
|
|
210
|
+
url.searchParams.set("os", describeOS())
|
|
211
|
+
url.searchParams.set("terminal", describeTerminal())
|
|
212
|
+
url.searchParams.set(
|
|
213
|
+
"reproduce",
|
|
214
|
+
"Reported automatically from the opencode crash screen. If you can, describe what you were doing when it crashed.",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Budget the stack against the fully URL-encoded length (not the raw length) so
|
|
218
|
+
// the final link stays under GitHub's practical limit; flag truncation so a
|
|
219
|
+
// clipped trace is obvious. searchParams.set handles encoding without throwing,
|
|
220
|
+
// so measuring url.toString() is both correct and safe on any input.
|
|
221
|
+
const MAX_URL_LENGTH = 6000
|
|
222
|
+
const marker = "\n... (truncated)"
|
|
223
|
+
const head = `The opencode TUI crashed with an unexpected error.\n\n**Error:** ${message}\n\n**Stack trace:**\n`
|
|
224
|
+
const setBody = (body: string) => url.searchParams.set("description", head + "```\n" + body + "\n```")
|
|
225
|
+
|
|
226
|
+
setBody(stack)
|
|
227
|
+
if (url.toString().length <= MAX_URL_LENGTH) return url
|
|
228
|
+
|
|
229
|
+
// Largest raw stack prefix whose encoded URL (with the marker) still fits.
|
|
230
|
+
let lo = 0
|
|
231
|
+
let hi = stack.length
|
|
232
|
+
while (lo < hi) {
|
|
233
|
+
const mid = Math.ceil((lo + hi) / 2)
|
|
234
|
+
setBody(stack.slice(0, mid) + marker)
|
|
235
|
+
if (url.toString().length <= MAX_URL_LENGTH) lo = mid
|
|
236
|
+
else hi = mid - 1
|
|
237
|
+
}
|
|
238
|
+
setBody(stack.slice(0, lo) + marker)
|
|
239
|
+
return url
|
|
240
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { RGBA, TextAttributes } from "@opentui/core"
|
|
2
|
+
import { For, type JSX } from "solid-js"
|
|
3
|
+
import { tint, useTheme } from "../context/theme"
|
|
4
|
+
import { logo } from "../logo"
|
|
5
|
+
|
|
6
|
+
export function Logo() {
|
|
7
|
+
const { theme } = useTheme()
|
|
8
|
+
|
|
9
|
+
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
|
10
|
+
const shadow = tint(theme.background, fg, 0.25)
|
|
11
|
+
const attrs = bold ? TextAttributes.BOLD : undefined
|
|
12
|
+
return Array.from(line).map((char) => {
|
|
13
|
+
if (char === "_") {
|
|
14
|
+
return (
|
|
15
|
+
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
|
16
|
+
{" "}
|
|
17
|
+
</text>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
if (char === "^") {
|
|
21
|
+
return (
|
|
22
|
+
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
|
23
|
+
▀
|
|
24
|
+
</text>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
if (char === "~") {
|
|
28
|
+
return (
|
|
29
|
+
<text fg={shadow} attributes={attrs} selectable={false}>
|
|
30
|
+
▀
|
|
31
|
+
</text>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (char === ",") {
|
|
35
|
+
return (
|
|
36
|
+
<text fg={shadow} attributes={attrs} selectable={false}>
|
|
37
|
+
▄
|
|
38
|
+
</text>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<text fg={fg} attributes={attrs} selectable={false}>
|
|
43
|
+
{char}
|
|
44
|
+
</text>
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<box>
|
|
51
|
+
<For each={logo.left}>
|
|
52
|
+
{(line, index) => (
|
|
53
|
+
<box flexDirection="row" gap={1}>
|
|
54
|
+
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
|
|
55
|
+
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
|
|
56
|
+
</box>
|
|
57
|
+
)}
|
|
58
|
+
</For>
|
|
59
|
+
</box>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useTheme } from "../context/theme"
|
|
2
|
+
|
|
3
|
+
export function PluginRouteMissing(props: { id: string; onHome: () => void }) {
|
|
4
|
+
const { theme } = useTheme()
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<box width="100%" height="100%" alignItems="center" justifyContent="center" flexDirection="column" gap={1}>
|
|
8
|
+
<text fg={theme.warning}>Unknown plugin route: {props.id}</text>
|
|
9
|
+
<box onMouseUp={props.onHome} backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
|
|
10
|
+
<text fg={theme.text}>go home</text>
|
|
11
|
+
</box>
|
|
12
|
+
</box>
|
|
13
|
+
)
|
|
14
|
+
}
|