Skip to content

Commit 7b89f06

Browse files
authored
feat(tui): prototype missing location recovery (#42353)
1 parent 3a41ee8 commit 7b89f06

8 files changed

Lines changed: 221 additions & 221 deletions

File tree

‎packages/drive/test/simulation/direct-cli.test.ts‎

Lines changed: 0 additions & 129 deletions
This file was deleted.

‎packages/tui/src/component/dialog-move-session.tsx‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type DialogMoveSessionProps = {
3131
onSelect: (selection: MoveSessionSelection) => void
3232
onCurrentChange?: (selection: MoveSessionSelection) => void
3333
initialDirectories?: ReadonlyArray<ProjectDirectory>
34+
fixture?: boolean
3435
initialRemoving?: string
3536
}
3637

@@ -75,7 +76,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
7576
})
7677

7778
const [directories, { refetch }] = createResource(
78-
() => (props.initialRemoving ? undefined : props.projectID),
79+
() => (props.fixture || props.initialRemoving ? undefined : props.projectID),
7980
async (projectID, info): Promise<ReadonlyArray<ProjectDirectory> | undefined> => {
8081
try {
8182
const requestLocation = { directory: location()?.directory || paths.cwd }
@@ -110,11 +111,9 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
110111
if (showError()) return
111112
const directory = currentDirectory()
112113
if (!directory) return
113-
return (
114-
directoryData()
115-
?.filter((root) => contains(root.directory, directory))
116-
.toSorted((a, b) => b.directory.length - a.directory.length)[0] ?? { directory }
117-
)
114+
return directoryData()
115+
?.filter((root) => contains(root.directory, directory))
116+
.toSorted((a, b) => b.directory.length - a.directory.length)[0]
118117
})
119118

120119
const options = createMemo<DialogSelectOption<MoveSessionSelection | undefined>[]>(() => {
@@ -123,7 +122,6 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
123122
const current = currentRoot()?.directory
124123
if (directories.loading && !data && !current) return []
125124
const roots = [...(data ?? [])]
126-
if (current && !roots.some((item) => item.directory === current)) roots.unshift({ directory: current })
127125
roots.sort((a, b) => {
128126
if (a.directory === current) return -1
129127
if (b.directory === current) return 1
@@ -139,15 +137,13 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
139137
(session) => session.projectID === props.projectID && session.subpath && ![".", "/"].includes(session.subpath),
140138
)
141139
.map((session) => session.location.directory)
140+
.filter((directory) => currentRoot() || directory !== currentDirectory())
142141
.filter((directory) => !roots.some((root) => root.directory === directory))
143142
.filter((directory, index, directories) => directories.indexOf(directory) === index)
144143
.map((location) => ({
145144
location,
146145
root: roots
147-
.filter((root) => {
148-
const relative = path.relative(root.directory, location)
149-
return relative && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative)
150-
})
146+
.filter((root) => contains(root.directory, location))
151147
.toSorted((a, b) => b.directory.length - a.directory.length)[0],
152148
}))
153149
.filter((item): item is { location: string; root: ProjectDirectory } => item.root !== undefined)
@@ -325,6 +321,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
325321
</box>
326322
}
327323
renderFilter={!showError()}
324+
flat={true}
328325
options={options()}
329326
emptyView={
330327
showError() ? (
@@ -357,7 +354,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
357354
}}
358355
onMove={() => setToDelete(undefined)}
359356
actions={
360-
showError()
357+
showError() || props.fixture
361358
? []
362359
: [
363360
{

‎packages/tui/src/context/location.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,37 @@ const context = createContext<{
77
readonly current: LocationGetOutput | undefined
88
// The target location as set, available before the server-synced info in `current` arrives.
99
readonly ref: LocationRef | undefined
10+
readonly error: { readonly location: LocationRef; readonly cause: unknown } | undefined
1011
set: (location?: LocationRef) => void
1112
}>()
1213

1314
export function LocationProvider(props: ParentProps) {
1415
const client = useClient()
1516
const data = useData()
1617
const [ref, setRef] = createSignal<LocationRef>()
18+
const [error, setError] = createSignal<{ readonly location: LocationRef; readonly cause: unknown }>()
19+
let generation = 0
1720
const current = createMemo(() => data.location.info(ref()))
1821

1922
function sync(location?: LocationRef) {
2023
if (!location) return
24+
const attempt = ++generation
2125
const defaultLocation = data.location.default()
2226
const target =
2327
location.directory === defaultLocation.directory && location.workspaceID === defaultLocation.workspaceID
2428
? undefined
2529
: location
26-
void data.location.sync(target).catch(() => undefined)
30+
setError(undefined)
31+
void data.location.sync(target).catch((cause) => {
32+
const current = ref()
33+
if (
34+
generation !== attempt ||
35+
current?.directory !== location.directory ||
36+
current.workspaceID !== location.workspaceID
37+
)
38+
return
39+
setError({ location, cause })
40+
})
2741
}
2842

2943
function set(location?: LocationRef) {
@@ -42,6 +56,9 @@ export function LocationProvider(props: ParentProps) {
4256
get ref() {
4357
return ref()
4458
},
59+
get error() {
60+
return error()
61+
},
4562
set,
4663
}}
4764
>

‎packages/tui/src/feature-plugins/system/storybook/index.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useTerminalDimensions } from "@opentui/solid"
33
import { createSignal, For, type JSX } from "solid-js"
44
import { StoryFooter } from "./footer"
55
import { sessionTabsStory } from "./session-tabs"
6+
import { sessionLocationMissingStory } from "./session-location-missing"
67

78
/**
89
* A story is a full-screen, fixture-driven simulation of a real production component. Stories own
@@ -14,7 +15,7 @@ export type Story = {
1415
render: (context: Plugin.Context) => JSX.Element
1516
}
1617

17-
const stories: Story[] = [sessionTabsStory]
18+
const stories: Story[] = [sessionTabsStory, sessionLocationMissingStory]
1819

1920
function Commands(props: { context: Plugin.Context }) {
2021
props.context.keymap.layer(() => ({
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { Plugin } from "@opencode-ai/plugin/tui"
2+
import { useTerminalDimensions } from "@opentui/solid"
3+
import { TextAttributes } from "@opentui/core"
4+
import { createSignal } from "solid-js"
5+
import { DialogMoveSession } from "../../../component/dialog-move-session"
6+
import { SessionLocationUnavailable } from "../../../routes/session/location-missing"
7+
import type { Story } from "./index"
8+
import { StoryFooter } from "./footer"
9+
10+
const directory = "/Users/kit/code/open-source/opencode-workerd-profile"
11+
12+
function SessionLocationMissingStory(props: { context: Plugin.Context }) {
13+
const dimensions = useTerminalDimensions()
14+
const theme = props.context.theme.contextual.elevated
15+
const [message, setMessage] = createSignal("Choose another directory to continue")
16+
const open = () =>
17+
props.context.ui.dialog.show(() => (
18+
<DialogMoveSession
19+
projectID="fixture-project"
20+
initialDirectories={[
21+
{ directory: "/Users/kit/code/open-source/opencode" },
22+
{
23+
directory: "/Users/kit/code/open-source/opencode-instruction-rename",
24+
strategy: "git_worktree",
25+
},
26+
]}
27+
fixture
28+
onSelect={(selection) => {
29+
if (selection.type !== "directory") return
30+
setMessage(`Selected ${selection.directory}`)
31+
props.context.ui.dialog.clear()
32+
}}
33+
/>
34+
))
35+
36+
props.context.keymap.layer(() => ({
37+
commands: [
38+
{
39+
bind: "escape",
40+
title: "Back to storybook",
41+
group: "Storybook",
42+
run: () => props.context.ui.router.navigate({ type: "plugin", name: "storybook" }),
43+
},
44+
],
45+
}))
46+
47+
return (
48+
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.default}>
49+
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
50+
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
51+
Workerd Modal workspace driver
52+
</text>
53+
<text fg={theme.text.subdued}>build · GPT-5.6 Sol (high)</text>
54+
<box height={1} />
55+
<text fg={theme.text.default}>You</text>
56+
<text fg={theme.text.subdued}>Test the mounted workspace and verify the deployment.</text>
57+
<box height={1} />
58+
<text fg={theme.text.default}>Build · GPT-5.6 Sol (high)</text>
59+
<text fg={theme.text.subdued}>The deployment is verified and the worktree is clean.</text>
60+
<box flexGrow={1} />
61+
<SessionLocationUnavailable directory={directory} onMove={open} />
62+
</box>
63+
<StoryFooter
64+
context={props.context}
65+
title="storybook / missing session directory"
66+
status={message()}
67+
controls={[
68+
{ shortcut: "enter", label: "confirm" },
69+
{ shortcut: "esc", label: "back" },
70+
]}
71+
/>
72+
</box>
73+
)
74+
}
75+
76+
export const sessionLocationMissingStory: Story = {
77+
id: "session-location-missing",
78+
title: "Missing session directory",
79+
render: (context) => <SessionLocationMissingStory context={context} />,
80+
}

‎packages/tui/src/routes/session/index.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import { createSingleFlight } from "../../util/single-flight"
108108
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
109109
import { generateThinkingSyntax } from "./thinking-syntax"
110110
import { createDelayedPresence } from "../../util/delayed-presence"
111+
import { SessionLocationMissing } from "./location-missing"
111112

112113
addDefaultParsers(parsers.parsers)
113114

@@ -1145,6 +1146,19 @@ export function Session() {
11451146
}}
11461147
</Show>
11471148
</Match>
1149+
<Match
1150+
when={
1151+
session() &&
1152+
currentLocation.error?.location.directory === session()!.location.directory &&
1153+
currentLocation.error?.location.workspaceID === session()!.location.workspaceID
1154+
}
1155+
>
1156+
<SessionLocationMissing
1157+
directory={session()!.location.directory}
1158+
projectID={session()!.projectID}
1159+
sessionID={route.sessionID}
1160+
/>
1161+
</Match>
11481162
<Match when={!disabled()}>
11491163
<Prompt
11501164
visible={true}

0 commit comments

Comments
 (0)