diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index 60ef6087ba..32342e7724 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
@@ -139,15 +139,10 @@ export function DialogSessionList() {
{desc}{" "}
- ■
+ ●
>
)
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
index 7ea513edee..6dcdabe0b9 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
@@ -139,7 +139,16 @@ export async function restoreWorkspaceSession(input: {
total: result.data.total,
})
- await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
+ input.project.workspace.set(input.workspaceID)
+
+ try {
+ await input.sync.bootstrap({ fatal: false })
+ } catch (e) {}
+
+ await Promise.all([
+ input.project.workspace.sync(),
+ input.sync.session.sync(input.sessionID),
+ ]).catch((err) => {
log.error("session restore refresh failed", {
workspaceID: input.workspaceID,
sessionID: input.sessionID,
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx
new file mode 100644
index 0000000000..0c2dd3e2f3
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx
@@ -0,0 +1,83 @@
+import { TextAttributes } from "@opentui/core"
+import { useKeyboard } from "@opentui/solid"
+import { createStore } from "solid-js/store"
+import { For } from "solid-js"
+import { useTheme } from "../context/theme"
+import { useDialog } from "../ui/dialog"
+
+export function DialogWorkspaceUnavailable(props: {
+ onRestore?: () => boolean | void | Promise
+}) {
+ const dialog = useDialog()
+ const { theme } = useTheme()
+ const [store, setStore] = createStore({
+ active: "restore" as "cancel" | "restore",
+ })
+
+ const options = ["cancel", "restore"] as const
+
+ async function confirm() {
+ if (store.active === "cancel") {
+ dialog.clear()
+ return
+ }
+ const result = await props.onRestore?.()
+ if (result === false) return
+ }
+
+ useKeyboard((evt) => {
+ if (evt.name === "return") {
+ evt.preventDefault()
+ evt.stopPropagation()
+ void confirm()
+ return
+ }
+ if (evt.name === "left") {
+ evt.preventDefault()
+ evt.stopPropagation()
+ setStore("active", "cancel")
+ return
+ }
+ if (evt.name === "right") {
+ evt.preventDefault()
+ evt.stopPropagation()
+ setStore("active", "restore")
+ }
+ })
+
+ return (
+
+
+
+ Workspace Unavailable
+
+ dialog.clear()}>
+ esc
+
+
+
+ This session is attached to a workspace that is no longer available.
+
+
+ Would you like to restore this session into a new workspace?
+
+
+
+ {(item) => (
+ {
+ setStore("active", item)
+ void confirm()
+ }}
+ >
+ {item}
+
+ )}
+
+
+
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index cf26ec1950..2e08e66a4a 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -9,6 +9,7 @@ import { tint, useTheme } from "@tui/context/theme"
import { EmptyBorder, SplitBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
+import { useProject } from "@tui/context/project"
import { useSync } from "@tui/context/sync"
import { useEvent } from "@tui/context/event"
import { MessageID, PartID } from "@/session/schema"
@@ -38,6 +39,8 @@ import { useKV } from "../../context/kv"
import { createFadeIn } from "../../util/signal"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
+import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create"
+import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
import { useArgs } from "@tui/context/args"
export type PromptProps = {
@@ -92,6 +95,7 @@ export function Prompt(props: PromptProps) {
const args = useArgs()
const sdk = useSDK()
const route = useRoute()
+ const project = useProject()
const sync = useSync()
const dialog = useDialog()
const toast = useToast()
@@ -241,9 +245,11 @@ export function Prompt(props: PromptProps) {
keybind: "input_submit",
category: "Prompt",
hidden: true,
- onSelect: (dialog) => {
+ onSelect: async (dialog) => {
if (!input.focused) return
- void submit()
+ const handled = await submit()
+ if (!handled) return
+
dialog.clear()
},
},
@@ -628,20 +634,48 @@ export function Prompt(props: PromptProps) {
setStore("prompt", "input", input.plainText)
syncExtmarksWithPromptParts()
}
- if (props.disabled) return
- if (autocomplete?.visible) return
- if (!store.prompt.input) return
+ if (props.disabled) return false
+ if (autocomplete?.visible) return false
+ if (!store.prompt.input) return false
const agent = local.agent.current()
- if (!agent) return
+ if (!agent) return false
const trimmed = store.prompt.input.trim()
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
void exit()
- return
+ return true
}
const selectedModel = local.model.current()
if (!selectedModel) {
void promptModelWarning()
- return
+ return false
+ }
+
+ const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined
+ const workspaceID = workspaceSession?.workspaceID
+ const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined
+ if (props.sessionID && workspaceID && workspaceStatus !== "connected") {
+ dialog.replace(() => (
+ {
+ dialog.replace(() => (
+
+ restoreWorkspaceSession({
+ dialog,
+ sdk,
+ sync,
+ project,
+ toast,
+ workspaceID: nextWorkspaceID,
+ sessionID: props.sessionID!,
+ })
+ }
+ />
+ ))
+ }}
+ />
+ ))
+ return false
}
let sessionID = props.sessionID
@@ -656,7 +690,7 @@ export function Prompt(props: PromptProps) {
variant: "error",
})
- return
+ return true
}
sessionID = res.data.id
@@ -770,6 +804,7 @@ export function Prompt(props: PromptProps) {
})
}, 50)
input.clear()
+ return true
}
const exit = useExit()
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
index 06bc270644..4a7b711a03 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -1,3 +1,4 @@
+import { useProject } from "@tui/context/project"
import { useSync } from "@tui/context/sync"
import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme"
@@ -8,10 +9,23 @@ import { TuiPluginRuntime } from "../../plugin"
import { getScrollAcceleration } from "../../util/scroll"
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
+ const project = useProject()
const sync = useSync()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const session = createMemo(() => sync.session.get(props.sessionID))
+ const workspaceStatus = () => {
+ const workspaceID = session()?.workspaceID
+ if (!workspaceID) return "error"
+ return project.workspace.status(workspaceID) ?? "error"
+ }
+ const workspaceLabel = () => {
+ const workspaceID = session()?.workspaceID
+ if (!workspaceID) return "unknown"
+ const info = project.workspace.get(workspaceID)
+ if (!info) return "unknown"
+ return `${info.type}: ${info.name}`
+ }
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
return (
@@ -48,6 +62,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
{session()!.title}
+
+
+ ●{" "}
+ {workspaceLabel()}
+
+
{session()!.share!.url}