@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,99 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core"
|
|
2
|
+
import { useTheme } from "../context/theme"
|
|
3
|
+
import { useDialog } from "../ui/dialog"
|
|
4
|
+
import { createStore } from "solid-js/store"
|
|
5
|
+
import { For } from "solid-js"
|
|
6
|
+
import { useBindings } from "../keymap"
|
|
7
|
+
|
|
8
|
+
export function DialogSessionDeleteFailed(props: {
|
|
9
|
+
session: string
|
|
10
|
+
workspace: string
|
|
11
|
+
onDelete?: () => boolean | void | Promise<boolean | void>
|
|
12
|
+
onRestore?: () => boolean | void | Promise<boolean | void>
|
|
13
|
+
onDone?: () => void
|
|
14
|
+
}) {
|
|
15
|
+
const dialog = useDialog()
|
|
16
|
+
const { theme } = useTheme()
|
|
17
|
+
const [store, setStore] = createStore({
|
|
18
|
+
active: "delete" as "delete" | "restore",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const options = [
|
|
22
|
+
{
|
|
23
|
+
id: "delete" as const,
|
|
24
|
+
title: "Delete workspace",
|
|
25
|
+
description: "Delete the workspace and all sessions attached to it.",
|
|
26
|
+
run: props.onDelete,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "restore" as const,
|
|
30
|
+
title: "Restore to new workspace",
|
|
31
|
+
description: "Try to restore this session into a new workspace.",
|
|
32
|
+
run: props.onRestore,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
async function confirm() {
|
|
37
|
+
const result = await options.find((item) => item.id === store.active)?.run?.()
|
|
38
|
+
if (result === false) return
|
|
39
|
+
props.onDone?.()
|
|
40
|
+
if (!props.onDone) dialog.clear()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
useBindings(() => ({
|
|
44
|
+
bindings: [
|
|
45
|
+
{ key: "return", desc: "Confirm recovery option", group: "Dialog", cmd: () => void confirm() },
|
|
46
|
+
{ key: "left", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
|
|
47
|
+
{ key: "up", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
|
|
48
|
+
{ key: "right", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
|
|
49
|
+
{ key: "down", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
|
|
50
|
+
],
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
55
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
56
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
57
|
+
Failed to Delete Session
|
|
58
|
+
</text>
|
|
59
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
60
|
+
esc
|
|
61
|
+
</text>
|
|
62
|
+
</box>
|
|
63
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
64
|
+
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
|
65
|
+
</text>
|
|
66
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
67
|
+
Choose how you want to recover this broken workspace session.
|
|
68
|
+
</text>
|
|
69
|
+
<box flexDirection="column" paddingBottom={1} gap={1}>
|
|
70
|
+
<For each={options}>
|
|
71
|
+
{(item) => (
|
|
72
|
+
<box
|
|
73
|
+
flexDirection="column"
|
|
74
|
+
paddingLeft={1}
|
|
75
|
+
paddingRight={1}
|
|
76
|
+
paddingTop={1}
|
|
77
|
+
paddingBottom={1}
|
|
78
|
+
backgroundColor={item.id === store.active ? theme.primary : undefined}
|
|
79
|
+
onMouseUp={() => {
|
|
80
|
+
setStore("active", item.id)
|
|
81
|
+
void confirm()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<text
|
|
85
|
+
attributes={TextAttributes.BOLD}
|
|
86
|
+
fg={item.id === store.active ? theme.selectedListItemText : theme.text}
|
|
87
|
+
>
|
|
88
|
+
{item.title}
|
|
89
|
+
</text>
|
|
90
|
+
<text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
|
|
91
|
+
{item.description}
|
|
92
|
+
</text>
|
|
93
|
+
</box>
|
|
94
|
+
)}
|
|
95
|
+
</For>
|
|
96
|
+
</box>
|
|
97
|
+
</box>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { useDialog } from "../ui/dialog"
|
|
2
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
3
|
+
import { useRoute } from "../context/route"
|
|
4
|
+
import { useSync } from "../context/sync"
|
|
5
|
+
import { createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js"
|
|
6
|
+
import path from "path"
|
|
7
|
+
import { Locale } from "../util/locale"
|
|
8
|
+
import { useProject } from "../context/project"
|
|
9
|
+
import { useTheme } from "../context/theme"
|
|
10
|
+
import { useSDK } from "../context/sdk"
|
|
11
|
+
import { useLocal } from "../context/local"
|
|
12
|
+
import { DialogSessionRename } from "./dialog-session-rename"
|
|
13
|
+
import { createDebouncedSignal } from "../util/signal"
|
|
14
|
+
import { useToast } from "../ui/toast"
|
|
15
|
+
import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
|
|
16
|
+
import { Spinner } from "./spinner"
|
|
17
|
+
import { errorMessage } from "../util/error"
|
|
18
|
+
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
|
|
19
|
+
import { useCommandShortcut } from "../keymap"
|
|
20
|
+
import { useEvent } from "../context/event"
|
|
21
|
+
|
|
22
|
+
type SessionListFilter = { scope?: "project"; path?: string }
|
|
23
|
+
|
|
24
|
+
export function createDialogSessionListQuery(input: { search?: string; filter: SessionListFilter }) {
|
|
25
|
+
const search = input.search?.trim()
|
|
26
|
+
return {
|
|
27
|
+
roots: true,
|
|
28
|
+
limit: search ? 30 : 100,
|
|
29
|
+
...(search ? { search } : {}),
|
|
30
|
+
...input.filter,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadDialogSessionList<T>(input: {
|
|
35
|
+
search?: string
|
|
36
|
+
filter: SessionListFilter
|
|
37
|
+
list: (query: ReturnType<typeof createDialogSessionListQuery>) => Promise<{ data?: T[] }>
|
|
38
|
+
}) {
|
|
39
|
+
return input.list(createDialogSessionListQuery(input)).then(
|
|
40
|
+
(result) => result.data,
|
|
41
|
+
() => undefined,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function DialogSessionList() {
|
|
46
|
+
const dialog = useDialog()
|
|
47
|
+
const route = useRoute()
|
|
48
|
+
const sync = useSync()
|
|
49
|
+
const project = useProject()
|
|
50
|
+
const { theme } = useTheme()
|
|
51
|
+
const sdk = useSDK()
|
|
52
|
+
const event = useEvent()
|
|
53
|
+
const local = useLocal()
|
|
54
|
+
const toast = useToast()
|
|
55
|
+
const [toDelete, setToDelete] = createSignal<string>()
|
|
56
|
+
const [deleted, setDeleted] = createSignal(new Set<string>())
|
|
57
|
+
const [search, setSearch] = createDebouncedSignal("", 150)
|
|
58
|
+
const deleteHint = useCommandShortcut("session.delete")
|
|
59
|
+
const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
|
|
60
|
+
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")
|
|
61
|
+
|
|
62
|
+
const [browseResults, { refetch: refetchBrowse }] = createResource(
|
|
63
|
+
() => sync.session.query(),
|
|
64
|
+
(filter) => loadDialogSessionList({ filter, list: (query) => sdk.client.session.list(query) }),
|
|
65
|
+
)
|
|
66
|
+
const [searchResults, { refetch }] = createResource(
|
|
67
|
+
() => ({ query: search(), filter: sync.session.query() }),
|
|
68
|
+
(input) => {
|
|
69
|
+
if (!input.query) return undefined
|
|
70
|
+
return loadDialogSessionList({
|
|
71
|
+
search: input.query,
|
|
72
|
+
filter: input.filter,
|
|
73
|
+
list: (query) => sdk.client.session.list(query),
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
|
79
|
+
const sessions = createMemo(() => {
|
|
80
|
+
const result = searchResults() ?? browseResults() ?? sync.data.session
|
|
81
|
+
const synced = new Map(sync.data.session.map((session) => [session.id, session]))
|
|
82
|
+
const ids = new Set(result.map((session) => session.id))
|
|
83
|
+
const extra = [currentSessionID(), ...local.session.pinned()].flatMap((id) => {
|
|
84
|
+
if (!id || ids.has(id)) return []
|
|
85
|
+
const session = synced.get(id)
|
|
86
|
+
if (session) ids.add(id)
|
|
87
|
+
return session ? [session] : []
|
|
88
|
+
})
|
|
89
|
+
const query = search().trim().toLowerCase()
|
|
90
|
+
return [...result.map((session) => synced.get(session.id) ?? session), ...extra]
|
|
91
|
+
.filter((session) => !deleted().has(session.id))
|
|
92
|
+
.filter((session) => !query || session.title.toLowerCase().includes(query))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
onCleanup(
|
|
96
|
+
event.on("session.deleted", (event) => {
|
|
97
|
+
setDeleted((current) => new Set(current).add(event.properties.info.id))
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
|
|
102
|
+
const workspace = project.workspace.get(session.workspaceID!)
|
|
103
|
+
const list = () => dialog.replace(() => <DialogSessionList />)
|
|
104
|
+
const warp = async (selection: WorkspaceSelection) => {
|
|
105
|
+
const workspaceID = await (async () => {
|
|
106
|
+
if (selection.type === "none") return null
|
|
107
|
+
if (selection.type === "existing") return selection.workspaceID
|
|
108
|
+
let result
|
|
109
|
+
try {
|
|
110
|
+
result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })
|
|
111
|
+
} catch (err) {
|
|
112
|
+
toast.show({
|
|
113
|
+
title: "Failed to create workspace",
|
|
114
|
+
message: errorMessage(err),
|
|
115
|
+
variant: "error",
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
const workspace = result?.data
|
|
120
|
+
if (!workspace) {
|
|
121
|
+
toast.show({
|
|
122
|
+
title: "Failed to create workspace",
|
|
123
|
+
message: errorMessage(result?.error ?? "no response"),
|
|
124
|
+
variant: "error",
|
|
125
|
+
})
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
await project.workspace.sync()
|
|
129
|
+
return workspace.id
|
|
130
|
+
})()
|
|
131
|
+
if (workspaceID === undefined) return
|
|
132
|
+
await warpWorkspaceSession({
|
|
133
|
+
dialog,
|
|
134
|
+
sdk,
|
|
135
|
+
sync,
|
|
136
|
+
project,
|
|
137
|
+
toast,
|
|
138
|
+
sourceWorkspaceID: session.workspaceID,
|
|
139
|
+
workspaceID,
|
|
140
|
+
sessionID: session.id,
|
|
141
|
+
copyChanges: false,
|
|
142
|
+
done: list,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
dialog.replace(() => (
|
|
146
|
+
<DialogSessionDeleteFailed
|
|
147
|
+
session={session.title}
|
|
148
|
+
workspace={workspace?.name ?? session.workspaceID!}
|
|
149
|
+
onDone={list}
|
|
150
|
+
onDelete={async () => {
|
|
151
|
+
const current = currentSessionID()
|
|
152
|
+
const info = current ? sync.data.session.find((item) => item.id === current) : undefined
|
|
153
|
+
const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
|
|
154
|
+
if (result.error) {
|
|
155
|
+
toast.show({
|
|
156
|
+
variant: "error",
|
|
157
|
+
title: "Failed to delete workspace",
|
|
158
|
+
message: errorMessage(result.error),
|
|
159
|
+
})
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
await project.workspace.sync()
|
|
163
|
+
await sync.session.refresh()
|
|
164
|
+
await refetchBrowse()
|
|
165
|
+
if (search()) await refetch()
|
|
166
|
+
if (info?.workspaceID === session.workspaceID) {
|
|
167
|
+
route.navigate({ type: "home" })
|
|
168
|
+
}
|
|
169
|
+
return true
|
|
170
|
+
}}
|
|
171
|
+
onRestore={() => {
|
|
172
|
+
void openWorkspaceSelect({
|
|
173
|
+
dialog,
|
|
174
|
+
sdk,
|
|
175
|
+
sync,
|
|
176
|
+
project,
|
|
177
|
+
toast,
|
|
178
|
+
onSelect: (selection) => {
|
|
179
|
+
void warp(selection)
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
return false
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function orderByRecency(sessionsList: NonNullable<ReturnType<typeof sessions>>) {
|
|
189
|
+
return sessionsList
|
|
190
|
+
.filter((x) => x.parentID === undefined)
|
|
191
|
+
.toSorted((a, b) => b.time.updated - a.time.updated)
|
|
192
|
+
.map((x) => x.id)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const browseOrder = createMemo(() => orderByRecency(browseResults() ?? sync.data.session))
|
|
196
|
+
|
|
197
|
+
const quickSwitchHint = createMemo(() => {
|
|
198
|
+
const first = quickSwitch1()
|
|
199
|
+
const last = quickSwitch9()
|
|
200
|
+
if (!first || !last) return undefined
|
|
201
|
+
return quickSwitchRange(first, last)
|
|
202
|
+
})
|
|
203
|
+
const quickSwitchFooterHints = createMemo(() => {
|
|
204
|
+
const hint = quickSwitchHint()
|
|
205
|
+
return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : []
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const options = createMemo(() => {
|
|
209
|
+
const today = new Date().toDateString()
|
|
210
|
+
const sessionMap = new Map(
|
|
211
|
+
sessions()
|
|
212
|
+
.filter((x) => x.parentID === undefined)
|
|
213
|
+
.map((x) => [x.id, x]),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const searchResult = searchResults()
|
|
217
|
+
const order = searchResult ? orderByRecency(sessions()) : browseOrder()
|
|
218
|
+
const current = currentSessionID()
|
|
219
|
+
const displayOrder = current && sessionMap.has(current) && !order.includes(current) ? [...order, current] : order
|
|
220
|
+
|
|
221
|
+
const pinned = local.session.pinned().filter((id) => sessionMap.has(id))
|
|
222
|
+
const pinnedSet = new Set(pinned)
|
|
223
|
+
const slotByID = new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))
|
|
224
|
+
|
|
225
|
+
function buildOption(id: string, category: string) {
|
|
226
|
+
const x = sessionMap.get(id)
|
|
227
|
+
if (!x) return undefined
|
|
228
|
+
const directory = x.path
|
|
229
|
+
? x.directory.endsWith(x.path)
|
|
230
|
+
? x.directory.slice(0, -x.path.length).replace(/\/$/, "")
|
|
231
|
+
: undefined
|
|
232
|
+
: x.directory
|
|
233
|
+
const footer =
|
|
234
|
+
directory && directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
|
|
235
|
+
|
|
236
|
+
const isDeleting = toDelete() === x.id
|
|
237
|
+
const status = sync.data.session_status?.[x.id]
|
|
238
|
+
const isWorking = status?.type === "busy" || status?.type === "retry"
|
|
239
|
+
const slot = slotByID.get(x.id)
|
|
240
|
+
const gutter = isWorking
|
|
241
|
+
? () => <Spinner />
|
|
242
|
+
: slot !== undefined
|
|
243
|
+
? () => <text fg={theme.accent}>{slot}</text>
|
|
244
|
+
: undefined
|
|
245
|
+
return {
|
|
246
|
+
title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,
|
|
247
|
+
bg: isDeleting ? theme.error : undefined,
|
|
248
|
+
value: x.id,
|
|
249
|
+
category,
|
|
250
|
+
footer,
|
|
251
|
+
gutter,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const remaining = displayOrder
|
|
256
|
+
.filter((id) => !pinnedSet.has(id))
|
|
257
|
+
.map((id) => {
|
|
258
|
+
const x = sessionMap.get(id)
|
|
259
|
+
if (!x) return undefined
|
|
260
|
+
const label = new Date(x.time.updated).toDateString()
|
|
261
|
+
return buildOption(id, label === today ? "Today" : label)
|
|
262
|
+
})
|
|
263
|
+
.filter((x) => x !== undefined)
|
|
264
|
+
|
|
265
|
+
return [...pinned.map((id) => buildOption(id, "Pinned")).filter((x) => x !== undefined), ...remaining]
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
onMount(() => {
|
|
269
|
+
dialog.setSize("large")
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<DialogSelect
|
|
274
|
+
title="Sessions"
|
|
275
|
+
options={options()}
|
|
276
|
+
skipFilter={true}
|
|
277
|
+
preserveSelection={true}
|
|
278
|
+
current={currentSessionID()}
|
|
279
|
+
onFilter={setSearch}
|
|
280
|
+
onMove={() => {
|
|
281
|
+
setToDelete(undefined)
|
|
282
|
+
}}
|
|
283
|
+
onSelect={(option) => {
|
|
284
|
+
route.navigate({
|
|
285
|
+
type: "session",
|
|
286
|
+
sessionID: option.value,
|
|
287
|
+
})
|
|
288
|
+
dialog.clear()
|
|
289
|
+
}}
|
|
290
|
+
actions={[
|
|
291
|
+
{
|
|
292
|
+
command: "session.pin.toggle",
|
|
293
|
+
title: "pin/unpin",
|
|
294
|
+
onTrigger: (option: { value: string }) => {
|
|
295
|
+
local.session.togglePin(option.value)
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
command: "session.delete",
|
|
300
|
+
title: "delete",
|
|
301
|
+
onTrigger: async (option) => {
|
|
302
|
+
if (toDelete() === option.value) {
|
|
303
|
+
const session = sessions().find((item) => item.id === option.value)
|
|
304
|
+
const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const result = await sdk.client.session.delete({
|
|
308
|
+
sessionID: option.value,
|
|
309
|
+
})
|
|
310
|
+
if (result.error) {
|
|
311
|
+
if (session?.workspaceID) {
|
|
312
|
+
recover(session)
|
|
313
|
+
} else {
|
|
314
|
+
toast.show({
|
|
315
|
+
variant: "error",
|
|
316
|
+
title: "Failed to delete session",
|
|
317
|
+
message: errorMessage(result.error),
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
setToDelete(undefined)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (session?.workspaceID) {
|
|
325
|
+
recover(session)
|
|
326
|
+
} else {
|
|
327
|
+
toast.show({
|
|
328
|
+
variant: "error",
|
|
329
|
+
title: "Failed to delete session",
|
|
330
|
+
message: errorMessage(err),
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
setToDelete(undefined)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
if (status && status !== "connected") {
|
|
337
|
+
await sync.session.refresh()
|
|
338
|
+
}
|
|
339
|
+
await refetchBrowse()
|
|
340
|
+
if (search()) await refetch()
|
|
341
|
+
setToDelete(undefined)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
setToDelete(option.value)
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
command: "session.rename",
|
|
349
|
+
title: "rename",
|
|
350
|
+
onTrigger: async (option) => {
|
|
351
|
+
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
]}
|
|
355
|
+
footerHints={quickSwitchFooterHints()}
|
|
356
|
+
/>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function quickSwitchRange(first: string, last: string) {
|
|
361
|
+
const prefix = first.slice(0, -1)
|
|
362
|
+
if (first.endsWith("1") && last === `${prefix}9`) return `${prefix}1-9`
|
|
363
|
+
return `${first} through ${last}`
|
|
364
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DialogPrompt } from "../ui/dialog-prompt"
|
|
2
|
+
import { useDialog } from "../ui/dialog"
|
|
3
|
+
import { useSync } from "../context/sync"
|
|
4
|
+
import { createMemo } from "solid-js"
|
|
5
|
+
import { useSDK } from "../context/sdk"
|
|
6
|
+
|
|
7
|
+
interface DialogSessionRenameProps {
|
|
8
|
+
session: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DialogSessionRename(props: DialogSessionRenameProps) {
|
|
12
|
+
const dialog = useDialog()
|
|
13
|
+
const sync = useSync()
|
|
14
|
+
const sdk = useSDK()
|
|
15
|
+
const session = createMemo(() => sync.session.get(props.session))
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<DialogPrompt
|
|
19
|
+
title="Rename Session"
|
|
20
|
+
value={session()?.title}
|
|
21
|
+
onConfirm={(value) => {
|
|
22
|
+
void sdk.client.session.update({
|
|
23
|
+
sessionID: props.session,
|
|
24
|
+
title: value,
|
|
25
|
+
})
|
|
26
|
+
dialog.clear()
|
|
27
|
+
}}
|
|
28
|
+
onCancel={() => dialog.clear()}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core"
|
|
2
|
+
import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
|
|
3
|
+
import { createResource, createMemo, createSignal } from "solid-js"
|
|
4
|
+
import { useDialog } from "../ui/dialog"
|
|
5
|
+
import { useSDK } from "../context/sdk"
|
|
6
|
+
import { useTheme } from "../context/theme"
|
|
7
|
+
import { errorMessage } from "../util/error"
|
|
8
|
+
|
|
9
|
+
export type DialogSkillProps = {
|
|
10
|
+
onSelect: (skill: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DialogSkill(props: DialogSkillProps) {
|
|
14
|
+
const dialog = useDialog()
|
|
15
|
+
const sdk = useSDK()
|
|
16
|
+
const { theme } = useTheme()
|
|
17
|
+
dialog.setSize("large")
|
|
18
|
+
|
|
19
|
+
const [loadError, setLoadError] = createSignal<unknown>()
|
|
20
|
+
|
|
21
|
+
const [skills] = createResource(() =>
|
|
22
|
+
sdk.client.app
|
|
23
|
+
.skills({}, { throwOnError: true })
|
|
24
|
+
.then((result) => result.data ?? [])
|
|
25
|
+
// Catch so the rejected resource never reaches the memo below: reading
|
|
26
|
+
// skills() in an errored state re-throws and tears down the dialog.
|
|
27
|
+
.catch((error) => {
|
|
28
|
+
setLoadError(error)
|
|
29
|
+
return undefined
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const showError = createMemo(() => Boolean(loadError()))
|
|
34
|
+
|
|
35
|
+
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
|
36
|
+
if (showError()) return []
|
|
37
|
+
const list = skills() ?? []
|
|
38
|
+
const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
|
|
39
|
+
return list.map((skill) => ({
|
|
40
|
+
title: skill.name.padEnd(maxWidth),
|
|
41
|
+
description: skill.description?.replace(/\s+/g, " ").trim(),
|
|
42
|
+
value: skill.name,
|
|
43
|
+
category: "Skills",
|
|
44
|
+
onSelect: () => {
|
|
45
|
+
props.onSelect(skill.name)
|
|
46
|
+
dialog.clear()
|
|
47
|
+
},
|
|
48
|
+
}))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<DialogSelect
|
|
53
|
+
title="Skills"
|
|
54
|
+
placeholder="Search skills..."
|
|
55
|
+
options={options()}
|
|
56
|
+
renderFilter={!showError()}
|
|
57
|
+
locked={showError()}
|
|
58
|
+
emptyView={
|
|
59
|
+
showError() ? (
|
|
60
|
+
<box paddingLeft={4} paddingRight={4}>
|
|
61
|
+
<text fg={theme.error} attributes={TextAttributes.BOLD}>
|
|
62
|
+
Could not load skills
|
|
63
|
+
</text>
|
|
64
|
+
<text fg={theme.textMuted}>{errorMessage(loadError())}</text>
|
|
65
|
+
</box>
|
|
66
|
+
) : undefined
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useDialog } from "../ui/dialog"
|
|
2
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
3
|
+
import { createMemo, createSignal } from "solid-js"
|
|
4
|
+
import { Locale } from "../util/locale"
|
|
5
|
+
import { useTheme } from "../context/theme"
|
|
6
|
+
import { usePromptStash, type StashEntry } from "./prompt/stash"
|
|
7
|
+
import { useCommandShortcut } from "../keymap"
|
|
8
|
+
|
|
9
|
+
function getRelativeTime(timestamp: number): string {
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
const diff = now - timestamp
|
|
12
|
+
const seconds = Math.floor(diff / 1000)
|
|
13
|
+
const minutes = Math.floor(seconds / 60)
|
|
14
|
+
const hours = Math.floor(minutes / 60)
|
|
15
|
+
const days = Math.floor(hours / 24)
|
|
16
|
+
|
|
17
|
+
if (seconds < 60) return "just now"
|
|
18
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
19
|
+
if (hours < 24) return `${hours}h ago`
|
|
20
|
+
if (days < 7) return `${days}d ago`
|
|
21
|
+
return Locale.datetime(timestamp)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStashPreview(input: string, maxLength: number = 50): string {
|
|
25
|
+
const firstLine = input.split("\n")[0].trim()
|
|
26
|
+
return Locale.truncate(firstLine, maxLength)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
|
30
|
+
const dialog = useDialog()
|
|
31
|
+
const stash = usePromptStash()
|
|
32
|
+
const { theme } = useTheme()
|
|
33
|
+
|
|
34
|
+
const [toDelete, setToDelete] = createSignal<number>()
|
|
35
|
+
const deleteHint = useCommandShortcut("stash.delete")
|
|
36
|
+
|
|
37
|
+
const options = createMemo(() => {
|
|
38
|
+
const entries = stash.list()
|
|
39
|
+
// Show most recent first
|
|
40
|
+
return entries
|
|
41
|
+
.map((entry, index) => {
|
|
42
|
+
const isDeleting = toDelete() === index
|
|
43
|
+
const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1
|
|
44
|
+
return {
|
|
45
|
+
title: isDeleting ? `Press ${deleteHint()} again to confirm` : getStashPreview(entry.input),
|
|
46
|
+
bg: isDeleting ? theme.error : undefined,
|
|
47
|
+
value: index,
|
|
48
|
+
description: getRelativeTime(entry.timestamp),
|
|
49
|
+
footer: lineCount > 1 ? `~${lineCount} lines` : undefined,
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.toReversed()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<DialogSelect
|
|
57
|
+
title="Stash"
|
|
58
|
+
options={options()}
|
|
59
|
+
onMove={() => {
|
|
60
|
+
setToDelete(undefined)
|
|
61
|
+
}}
|
|
62
|
+
onSelect={(option) => {
|
|
63
|
+
const entries = stash.list()
|
|
64
|
+
const entry = entries[option.value]
|
|
65
|
+
if (entry) {
|
|
66
|
+
stash.remove(option.value)
|
|
67
|
+
props.onSelect(entry)
|
|
68
|
+
}
|
|
69
|
+
dialog.clear()
|
|
70
|
+
}}
|
|
71
|
+
actions={[
|
|
72
|
+
{
|
|
73
|
+
command: "stash.delete",
|
|
74
|
+
title: "delete",
|
|
75
|
+
onTrigger: (option) => {
|
|
76
|
+
if (toDelete() === option.value) {
|
|
77
|
+
stash.remove(option.value)
|
|
78
|
+
setToDelete(undefined)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
setToDelete(option.value)
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|