Skip to content

Commit b275b85

Browse files
authored
feat(tui): minor UX improvements for workspaces (#23146)
1 parent 467be08 commit b275b85

5 files changed

Lines changed: 159 additions & 17 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,10 @@ export function DialogSessionList() {
139139
{desc}{" "}
140140
<span
141141
style={{
142-
fg:
143-
workspaceStatus === "error"
144-
? theme.error
145-
: workspaceStatus === "disconnected"
146-
? theme.textMuted
147-
: theme.success,
142+
fg: workspaceStatus === "connected" ? theme.success : theme.error,
148143
}}
149144
>
150-
145+
151146
</span>
152147
</>
153148
)

packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,16 @@ export async function restoreWorkspaceSession(input: {
139139
total: result.data.total,
140140
})
141141

142-
await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
142+
input.project.workspace.set(input.workspaceID)
143+
144+
try {
145+
await input.sync.bootstrap({ fatal: false })
146+
} catch (e) {}
147+
148+
await Promise.all([
149+
input.project.workspace.sync(),
150+
input.sync.session.sync(input.sessionID),
151+
]).catch((err) => {
143152
log.error("session restore refresh failed", {
144153
workspaceID: input.workspaceID,
145154
sessionID: input.sessionID,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { TextAttributes } from "@opentui/core"
2+
import { useKeyboard } from "@opentui/solid"
3+
import { createStore } from "solid-js/store"
4+
import { For } from "solid-js"
5+
import { useTheme } from "../context/theme"
6+
import { useDialog } from "../ui/dialog"
7+
8+
export function DialogWorkspaceUnavailable(props: {
9+
onRestore?: () => boolean | void | Promise<boolean | void>
10+
}) {
11+
const dialog = useDialog()
12+
const { theme } = useTheme()
13+
const [store, setStore] = createStore({
14+
active: "restore" as "cancel" | "restore",
15+
})
16+
17+
const options = ["cancel", "restore"] as const
18+
19+
async function confirm() {
20+
if (store.active === "cancel") {
21+
dialog.clear()
22+
return
23+
}
24+
const result = await props.onRestore?.()
25+
if (result === false) return
26+
}
27+
28+
useKeyboard((evt) => {
29+
if (evt.name === "return") {
30+
evt.preventDefault()
31+
evt.stopPropagation()
32+
void confirm()
33+
return
34+
}
35+
if (evt.name === "left") {
36+
evt.preventDefault()
37+
evt.stopPropagation()
38+
setStore("active", "cancel")
39+
return
40+
}
41+
if (evt.name === "right") {
42+
evt.preventDefault()
43+
evt.stopPropagation()
44+
setStore("active", "restore")
45+
}
46+
})
47+
48+
return (
49+
<box paddingLeft={2} paddingRight={2} gap={1}>
50+
<box flexDirection="row" justifyContent="space-between">
51+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
52+
Workspace Unavailable
53+
</text>
54+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
55+
esc
56+
</text>
57+
</box>
58+
<text fg={theme.textMuted} wrapMode="word">
59+
This session is attached to a workspace that is no longer available.
60+
</text>
61+
<text fg={theme.textMuted} wrapMode="word">
62+
Would you like to restore this session into a new workspace?
63+
</text>
64+
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
65+
<For each={options}>
66+
{(item) => (
67+
<box
68+
paddingLeft={2}
69+
paddingRight={2}
70+
backgroundColor={item === store.active ? theme.primary : undefined}
71+
onMouseUp={() => {
72+
setStore("active", item)
73+
void confirm()
74+
}}
75+
>
76+
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
77+
</box>
78+
)}
79+
</For>
80+
</box>
81+
</box>
82+
)
83+
}

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { tint, useTheme } from "@tui/context/theme"
99
import { EmptyBorder, SplitBorder } from "@tui/component/border"
1010
import { useSDK } from "@tui/context/sdk"
1111
import { useRoute } from "@tui/context/route"
12+
import { useProject } from "@tui/context/project"
1213
import { useSync } from "@tui/context/sync"
1314
import { useEvent } from "@tui/context/event"
1415
import { MessageID, PartID } from "@/session/schema"
@@ -38,6 +39,8 @@ import { useKV } from "../../context/kv"
3839
import { createFadeIn } from "../../util/signal"
3940
import { useTextareaKeybindings } from "../textarea-keybindings"
4041
import { DialogSkill } from "../dialog-skill"
42+
import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create"
43+
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
4144
import { useArgs } from "@tui/context/args"
4245

4346
export type PromptProps = {
@@ -92,6 +95,7 @@ export function Prompt(props: PromptProps) {
9295
const args = useArgs()
9396
const sdk = useSDK()
9497
const route = useRoute()
98+
const project = useProject()
9599
const sync = useSync()
96100
const dialog = useDialog()
97101
const toast = useToast()
@@ -241,9 +245,11 @@ export function Prompt(props: PromptProps) {
241245
keybind: "input_submit",
242246
category: "Prompt",
243247
hidden: true,
244-
onSelect: (dialog) => {
248+
onSelect: async (dialog) => {
245249
if (!input.focused) return
246-
void submit()
250+
const handled = await submit()
251+
if (!handled) return
252+
247253
dialog.clear()
248254
},
249255
},
@@ -628,20 +634,48 @@ export function Prompt(props: PromptProps) {
628634
setStore("prompt", "input", input.plainText)
629635
syncExtmarksWithPromptParts()
630636
}
631-
if (props.disabled) return
632-
if (autocomplete?.visible) return
633-
if (!store.prompt.input) return
637+
if (props.disabled) return false
638+
if (autocomplete?.visible) return false
639+
if (!store.prompt.input) return false
634640
const agent = local.agent.current()
635-
if (!agent) return
641+
if (!agent) return false
636642
const trimmed = store.prompt.input.trim()
637643
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
638644
void exit()
639-
return
645+
return true
640646
}
641647
const selectedModel = local.model.current()
642648
if (!selectedModel) {
643649
void promptModelWarning()
644-
return
650+
return false
651+
}
652+
653+
const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined
654+
const workspaceID = workspaceSession?.workspaceID
655+
const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined
656+
if (props.sessionID && workspaceID && workspaceStatus !== "connected") {
657+
dialog.replace(() => (
658+
<DialogWorkspaceUnavailable
659+
onRestore={() => {
660+
dialog.replace(() => (
661+
<DialogWorkspaceCreate
662+
onSelect={(nextWorkspaceID) =>
663+
restoreWorkspaceSession({
664+
dialog,
665+
sdk,
666+
sync,
667+
project,
668+
toast,
669+
workspaceID: nextWorkspaceID,
670+
sessionID: props.sessionID!,
671+
})
672+
}
673+
/>
674+
))
675+
}}
676+
/>
677+
))
678+
return false
645679
}
646680

647681
let sessionID = props.sessionID
@@ -656,7 +690,7 @@ export function Prompt(props: PromptProps) {
656690
variant: "error",
657691
})
658692

659-
return
693+
return true
660694
}
661695

662696
sessionID = res.data.id
@@ -770,6 +804,7 @@ export function Prompt(props: PromptProps) {
770804
})
771805
}, 50)
772806
input.clear()
807+
return true
773808
}
774809
const exit = useExit()
775810

packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useProject } from "@tui/context/project"
12
import { useSync } from "@tui/context/sync"
23
import { createMemo, Show } from "solid-js"
34
import { useTheme } from "../../context/theme"
@@ -8,10 +9,23 @@ import { TuiPluginRuntime } from "../../plugin"
89
import { getScrollAcceleration } from "../../util/scroll"
910

1011
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
12+
const project = useProject()
1113
const sync = useSync()
1214
const { theme } = useTheme()
1315
const tuiConfig = useTuiConfig()
1416
const session = createMemo(() => sync.session.get(props.sessionID))
17+
const workspaceStatus = () => {
18+
const workspaceID = session()?.workspaceID
19+
if (!workspaceID) return "error"
20+
return project.workspace.status(workspaceID) ?? "error"
21+
}
22+
const workspaceLabel = () => {
23+
const workspaceID = session()?.workspaceID
24+
if (!workspaceID) return "unknown"
25+
const info = project.workspace.get(workspaceID)
26+
if (!info) return "unknown"
27+
return `${info.type}: ${info.name}`
28+
}
1529
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
1630

1731
return (
@@ -48,6 +62,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
4862
<text fg={theme.text}>
4963
<b>{session()!.title}</b>
5064
</text>
65+
<Show when={session()!.workspaceID}>
66+
<text fg={theme.textMuted}>
67+
<span style={{ fg: workspaceStatus() === "connected" ? theme.success : theme.error }}></span>{" "}
68+
{workspaceLabel()}
69+
</text>
70+
</Show>
5171
<Show when={session()!.share?.url}>
5272
<text fg={theme.textMuted}>{session()!.share!.url}</text>
5373
</Show>

0 commit comments

Comments
 (0)