Skip to content

Commit a81ff3a

Browse files
alltomatosclaude
andcommitted
refactor(app): extract workspace delete/reset dialogs from layout.tsx
Pulls DialogDeleteWorkspace/DialogResetWorkspace (~220 lines) out into their own file, passing the outer component's deleteWorkspace/ resetWorkspace/navigate closures in as props instead of relying on JS closure capture. Partial progress on #13 — the rest of layout.tsx is one large component-scoped function (no other top-level seams), so further splitting needs deeper restructuring, not just extraction. Co-Authored-By: Claude Sonnet 5 <[email protected]>
1 parent 50fde28 commit a81ff3a

2 files changed

Lines changed: 166 additions & 133 deletions

File tree

‎packages/app/src/pages/layout.tsx‎

Lines changed: 14 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
drainPendingDeepLinks,
7676
} from "./layout/deep-links"
7777
import { createInlineEditorController } from "./layout/inline-editor"
78+
import { DialogDeleteWorkspace, DialogResetWorkspace } from "./layout/dialog-workspace-actions"
7879
import {
7980
LocalWorkspace,
8081
SortableWorkspace,
@@ -1526,138 +1527,11 @@ export default function LegacyLayout(props: ParentProps) {
15261527
})
15271528
}
15281529

1529-
function DialogDeleteWorkspace(props: { root: string; directory: string }) {
1530-
const name = createMemo(() => getFilename(props.directory))
1531-
const [data, setData] = createStore({
1532-
status: "loading" as "loading" | "ready" | "error",
1533-
dirty: false,
1534-
})
1535-
1536-
onMount(() => {
1537-
serverSDK()
1538-
.api.vcs.status({ location: { directory: props.directory } })
1539-
.then((result) => {
1540-
const files = result.data
1541-
const dirty = files.length > 0
1542-
setData({ status: "ready", dirty })
1543-
})
1544-
.catch(() => {
1545-
setData({ status: "error", dirty: false })
1546-
})
1547-
})
1548-
1549-
const handleDelete = () => {
1550-
const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory)
1551-
if (leaveDeletedWorkspace) {
1552-
navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
1553-
}
1554-
dialog.close()
1555-
void deleteWorkspace(props.root, props.directory, leaveDeletedWorkspace)
1556-
}
1557-
1558-
const description = () => {
1559-
if (data.status === "loading") return language.t("workspace.status.checking")
1560-
if (data.status === "error") return language.t("workspace.status.error")
1561-
if (!data.dirty) return language.t("workspace.status.clean")
1562-
return language.t("workspace.status.dirty")
1563-
}
1564-
1565-
return (
1566-
<Dialog title={language.t("workspace.delete.title")} fit>
1567-
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
1568-
<div class="flex flex-col gap-1">
1569-
<span class="text-14-regular text-text-strong">
1570-
{language.t("workspace.delete.confirm", { name: name() })}
1571-
</span>
1572-
<span class="text-12-regular text-text-weak">{description()}</span>
1573-
</div>
1574-
<div class="flex justify-end gap-2">
1575-
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
1576-
{language.t("common.cancel")}
1577-
</Button>
1578-
<Button variant="primary" size="large" disabled={data.status === "loading"} onClick={handleDelete}>
1579-
{language.t("workspace.delete.button")}
1580-
</Button>
1581-
</div>
1582-
</div>
1583-
</Dialog>
1584-
)
1585-
}
1586-
1587-
function DialogResetWorkspace(props: { root: string; directory: string }) {
1588-
const name = createMemo(() => getFilename(props.directory))
1589-
const [state, setState] = createStore({
1590-
status: "loading" as "loading" | "ready" | "error",
1591-
dirty: false,
1592-
sessions: [] as Session[],
1593-
})
1594-
1595-
const refresh = async () => {
1596-
const sessions = await listAllSessions(serverSDK().api.session, {
1597-
directory: props.directory,
1598-
order: "desc",
1599-
}).catch(() => [])
1600-
const active = sessions.filter((session) => session.time.archived === undefined)
1601-
setState({ sessions: active })
1602-
}
1603-
1604-
onMount(() => {
1605-
serverSDK()
1606-
.api.vcs.status({ location: { directory: props.directory } })
1607-
.then((result) => {
1608-
const files = result.data
1609-
const dirty = files.length > 0
1610-
setState({ status: "ready", dirty })
1611-
void refresh()
1612-
})
1613-
.catch(() => {
1614-
setState({ status: "error", dirty: false })
1615-
})
1616-
})
1617-
1618-
const handleReset = () => {
1619-
dialog.close()
1620-
void resetWorkspace(props.root, props.directory)
1621-
}
1622-
1623-
const archivedCount = () => state.sessions.length
1624-
1625-
const description = () => {
1626-
if (state.status === "loading") return language.t("workspace.status.checking")
1627-
if (state.status === "error") return language.t("workspace.status.error")
1628-
if (!state.dirty) return language.t("workspace.status.clean")
1629-
return language.t("workspace.status.dirty")
1630-
}
1631-
1632-
const archivedLabel = () => {
1633-
const count = archivedCount()
1634-
if (count === 0) return language.t("workspace.reset.archived.none")
1635-
if (count === 1) return language.t("workspace.reset.archived.one")
1636-
return language.t("workspace.reset.archived.many", { count })
1530+
const handleDeleteWorkspace = (root: string, directory: string, leaveDeletedWorkspace: boolean) => {
1531+
if (leaveDeletedWorkspace) {
1532+
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
16371533
}
1638-
1639-
return (
1640-
<Dialog title={language.t("workspace.reset.title")} fit>
1641-
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
1642-
<div class="flex flex-col gap-1">
1643-
<span class="text-14-regular text-text-strong">
1644-
{language.t("workspace.reset.confirm", { name: name() })}
1645-
</span>
1646-
<span class="text-12-regular text-text-weak">
1647-
{description()} {archivedLabel()} {language.t("workspace.reset.note")}
1648-
</span>
1649-
</div>
1650-
<div class="flex justify-end gap-2">
1651-
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
1652-
{language.t("common.cancel")}
1653-
</Button>
1654-
<Button variant="primary" size="large" disabled={state.status === "loading"} onClick={handleReset}>
1655-
{language.t("workspace.reset.button")}
1656-
</Button>
1657-
</div>
1658-
</div>
1659-
</Dialog>
1660-
)
1534+
void deleteWorkspace(root, directory, leaveDeletedWorkspace)
16611535
}
16621536

16631537
const activeRoute = {
@@ -1890,9 +1764,16 @@ export default function LegacyLayout(props: ParentProps) {
18901764
workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local,
18911765
setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value),
18921766
showResetWorkspaceDialog: (root, directory) =>
1893-
dialog.show(() => <DialogResetWorkspace root={root} directory={directory} />),
1767+
dialog.show(() => <DialogResetWorkspace root={root} directory={directory} onReset={resetWorkspace} />),
18941768
showDeleteWorkspaceDialog: (root, directory) =>
1895-
dialog.show(() => <DialogDeleteWorkspace root={root} directory={directory} />),
1769+
dialog.show(() => (
1770+
<DialogDeleteWorkspace
1771+
root={root}
1772+
directory={directory}
1773+
isCurrent={() => !!params.dir && pathKey(currentDir()) === pathKey(directory)}
1774+
onDelete={handleDeleteWorkspace}
1775+
/>
1776+
)),
18961777
setScrollContainerRef: (el, mobile) => {
18971778
if (!mobile) scrollContainerRef = el
18981779
},
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { createMemo, onMount } from "solid-js"
2+
import { createStore } from "solid-js/store"
3+
import type { Session } from "@opencode-ai/sdk/v2/client"
4+
import { Button } from "@opencode-ai/ui/button"
5+
import { Dialog } from "@opencode-ai/ui/dialog"
6+
import { getFilename } from "@opencode-ai/core/util/path"
7+
import { useDialog } from "@opencode-ai/ui/context/dialog"
8+
import { useLanguage } from "@/context/language"
9+
import { useServerSDK } from "@/context/server-sdk"
10+
import { listAllSessions } from "@/utils/session"
11+
12+
export function DialogDeleteWorkspace(props: {
13+
root: string
14+
directory: string
15+
isCurrent: () => boolean
16+
onDelete: (root: string, directory: string, leaveDeletedWorkspace: boolean) => void
17+
}) {
18+
const dialog = useDialog()
19+
const language = useLanguage()
20+
const serverSDK = useServerSDK()
21+
const name = createMemo(() => getFilename(props.directory))
22+
const [data, setData] = createStore({
23+
status: "loading" as "loading" | "ready" | "error",
24+
dirty: false,
25+
})
26+
27+
onMount(() => {
28+
serverSDK()
29+
.api.vcs.status({ location: { directory: props.directory } })
30+
.then((result) => {
31+
const files = result.data
32+
const dirty = files.length > 0
33+
setData({ status: "ready", dirty })
34+
})
35+
.catch(() => {
36+
setData({ status: "error", dirty: false })
37+
})
38+
})
39+
40+
const handleDelete = () => {
41+
const leaveDeletedWorkspace = props.isCurrent()
42+
dialog.close()
43+
props.onDelete(props.root, props.directory, leaveDeletedWorkspace)
44+
}
45+
46+
const description = () => {
47+
if (data.status === "loading") return language.t("workspace.status.checking")
48+
if (data.status === "error") return language.t("workspace.status.error")
49+
if (!data.dirty) return language.t("workspace.status.clean")
50+
return language.t("workspace.status.dirty")
51+
}
52+
53+
return (
54+
<Dialog title={language.t("workspace.delete.title")} fit>
55+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
56+
<div class="flex flex-col gap-1">
57+
<span class="text-14-regular text-text-strong">{language.t("workspace.delete.confirm", { name: name() })}</span>
58+
<span class="text-12-regular text-text-weak">{description()}</span>
59+
</div>
60+
<div class="flex justify-end gap-2">
61+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
62+
{language.t("common.cancel")}
63+
</Button>
64+
<Button variant="primary" size="large" disabled={data.status === "loading"} onClick={handleDelete}>
65+
{language.t("workspace.delete.button")}
66+
</Button>
67+
</div>
68+
</div>
69+
</Dialog>
70+
)
71+
}
72+
73+
export function DialogResetWorkspace(props: {
74+
root: string
75+
directory: string
76+
onReset: (root: string, directory: string) => void
77+
}) {
78+
const dialog = useDialog()
79+
const language = useLanguage()
80+
const serverSDK = useServerSDK()
81+
const name = createMemo(() => getFilename(props.directory))
82+
const [state, setState] = createStore({
83+
status: "loading" as "loading" | "ready" | "error",
84+
dirty: false,
85+
sessions: [] as Session[],
86+
})
87+
88+
const refresh = async () => {
89+
const sessions = await listAllSessions(serverSDK().api.session, {
90+
directory: props.directory,
91+
order: "desc",
92+
}).catch(() => [])
93+
const active = sessions.filter((session) => session.time.archived === undefined)
94+
setState({ sessions: active })
95+
}
96+
97+
onMount(() => {
98+
serverSDK()
99+
.api.vcs.status({ location: { directory: props.directory } })
100+
.then((result) => {
101+
const files = result.data
102+
const dirty = files.length > 0
103+
setState({ status: "ready", dirty })
104+
void refresh()
105+
})
106+
.catch(() => {
107+
setState({ status: "error", dirty: false })
108+
})
109+
})
110+
111+
const handleReset = () => {
112+
dialog.close()
113+
props.onReset(props.root, props.directory)
114+
}
115+
116+
const archivedCount = () => state.sessions.length
117+
118+
const description = () => {
119+
if (state.status === "loading") return language.t("workspace.status.checking")
120+
if (state.status === "error") return language.t("workspace.status.error")
121+
if (!state.dirty) return language.t("workspace.status.clean")
122+
return language.t("workspace.status.dirty")
123+
}
124+
125+
const archivedLabel = () => {
126+
const count = archivedCount()
127+
if (count === 0) return language.t("workspace.reset.archived.none")
128+
if (count === 1) return language.t("workspace.reset.archived.one")
129+
return language.t("workspace.reset.archived.many", { count })
130+
}
131+
132+
return (
133+
<Dialog title={language.t("workspace.reset.title")} fit>
134+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
135+
<div class="flex flex-col gap-1">
136+
<span class="text-14-regular text-text-strong">{language.t("workspace.reset.confirm", { name: name() })}</span>
137+
<span class="text-12-regular text-text-weak">
138+
{description()} {archivedLabel()} {language.t("workspace.reset.note")}
139+
</span>
140+
</div>
141+
<div class="flex justify-end gap-2">
142+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
143+
{language.t("common.cancel")}
144+
</Button>
145+
<Button variant="primary" size="large" disabled={state.status === "loading"} onClick={handleReset}>
146+
{language.t("workspace.reset.button")}
147+
</Button>
148+
</div>
149+
</div>
150+
</Dialog>
151+
)
152+
}

0 commit comments

Comments
 (0)