summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-20 05:06:05 -0600
committerAdam <[email protected]>2026-01-20 05:39:54 -0600
commit0596b02f198ebe43703c3702f45eb2f1acdda431 (patch)
tree4714034a023a4e33d59b11931671960ac2c871df
parent5145b72c4abd71d51d493d4ee3744fa979d36c8b (diff)
downloadopencode-0596b02f198ebe43703c3702f45eb2f1acdda431.tar.gz
opencode-0596b02f198ebe43703c3702f45eb2f1acdda431.zip
chore: cleanup
-rw-r--r--packages/app/src/pages/layout.tsx153
-rw-r--r--packages/desktop/src/index.tsx23
2 files changed, 122 insertions, 54 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index dc716c030..b46758049 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -76,6 +76,7 @@ export default function Layout(props: ParentProps) {
activeWorkspace: undefined as string | undefined,
workspaceOrder: {} as Record<string, string[]>,
workspaceName: {} as Record<string, string>,
+ workspaceBranchName: {} as Record<string, Record<string, string>>,
workspaceExpanded: {} as Record<string, boolean>,
}),
)
@@ -198,7 +199,10 @@ export default function Layout(props: ParentProps) {
value={editorValue()}
class={props.class}
onInput={(event) => setEditor("value", event.currentTarget.value)}
- onKeyDown={(event) => editorKeyDown(event, props.onSave)}
+ onKeyDown={(event) => {
+ event.stopPropagation()
+ editorKeyDown(event, props.onSave)
+ }}
onBlur={() => closeEditor()}
onPointerDown={stopPropagation}
onClick={stopPropagation}
@@ -458,9 +462,27 @@ export default function Layout(props: ParentProps) {
),
)
- const workspaceName = (directory: string) => store.workspaceName[directory]
- const workspaceLabel = (directory: string, branch?: string) =>
- workspaceName(directory) ?? branch ?? getFilename(directory)
+ const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "")
+
+ const workspaceName = (directory: string, projectId?: string, branch?: string) => {
+ const key = workspaceKey(directory)
+ const direct = store.workspaceName[key] ?? store.workspaceName[directory]
+ if (direct) return direct
+ if (!projectId) return
+ if (!branch) return
+ return store.workspaceBranchName[projectId]?.[branch]
+ }
+
+ const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
+ const key = workspaceKey(directory)
+ setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next }))
+ if (!projectId) return
+ if (!branch) return
+ setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next }))
+ }
+
+ const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
+ workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
const isWorkspaceEditing = () => editor.active.startsWith("workspace:")
@@ -885,10 +907,10 @@ export default function Layout(props: ParentProps) {
})
}
- const renameWorkspace = (directory: string, next: string) => {
- const current = workspaceName(directory) ?? getFilename(directory)
+ const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
+ const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
if (current === next) return
- setStore("workspaceName", directory, next)
+ setWorkspaceName(directory, next, projectId, branch)
}
function closeProject(directory: string) {
@@ -1491,7 +1513,7 @@ export default function Layout(props: ParentProps) {
const [workspaceStore] = globalSync.child(directory)
const kind = directory === project.worktree ? "local" : "sandbox"
- const name = workspaceLabel(directory, workspaceStore.vcs?.branch)
+ const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
return `${kind} : ${name}`
})
@@ -1508,6 +1530,7 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory)
const [menuOpen, setMenuOpen] = createSignal(false)
+ const [pendingRename, setPendingRename] = createSignal(false)
const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() =>
workspaceStore.session
@@ -1517,8 +1540,9 @@ export default function Layout(props: ParentProps) {
)
const local = createMemo(() => props.directory === props.project.worktree)
const workspaceValue = createMemo(() => {
- const name = workspaceStore.vcs?.branch ?? getFilename(props.directory)
- return workspaceName(props.directory) ?? name
+ const branch = workspaceStore.vcs?.branch
+ const name = branch ?? getFilename(props.directory)
+ return workspaceName(props.directory, props.project.id, branch) ?? name
})
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? true)
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
@@ -1537,6 +1561,44 @@ export default function Layout(props: ParentProps) {
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
}
+ const header = () => (
+ <div class="flex items-center gap-1 min-w-0 flex-1">
+ <div class="flex items-center justify-center shrink-0 size-6">
+ <Icon name="branch" size="small" />
+ </div>
+ <span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span>
+ <Show
+ when={!local()}
+ fallback={
+ <span class="text-14-medium text-text-base min-w-0 truncate">
+ {workspaceStore.vcs?.branch ?? getFilename(props.directory)}
+ </span>
+ }
+ >
+ <InlineEditor
+ id={`workspace:${props.directory}`}
+ value={workspaceValue}
+ onSave={(next) => {
+ const trimmed = next.trim()
+ if (!trimmed) return
+ renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch)
+ setEditor("value", workspaceValue())
+ }}
+ class="text-14-medium text-text-base min-w-0 truncate"
+ displayClass="text-14-medium text-text-base min-w-0 truncate"
+ editing={workspaceEditActive()}
+ stopPropagation={false}
+ openOnDblClick={false}
+ />
+ </Show>
+ <Icon
+ name={open() ? "chevron-down" : "chevron-right"}
+ size="small"
+ class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
+ />
+ </div>
+ )
+
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
@@ -1544,43 +1606,18 @@ export default function Layout(props: ParentProps) {
<div class="px-2 py-1">
<div class="group/workspace relative">
<div class="flex items-center gap-1">
- <Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
- <div class="flex items-center gap-1 min-w-0 flex-1">
- <div class="flex items-center justify-center shrink-0 size-6">
- <Icon name="branch" size="small" />
- </div>
- <span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span>
- <Show
- when={!local()}
- fallback={
- <span class="text-14-medium text-text-base min-w-0 truncate">
- {workspaceStore.vcs?.branch ?? getFilename(props.directory)}
- </span>
- }
- >
- <InlineEditor
- id={`workspace:${props.directory}`}
- value={workspaceValue}
- onSave={(next) => {
- const trimmed = next.trim()
- if (!trimmed) return
- renameWorkspace(props.directory, trimmed)
- setEditor("value", workspaceValue())
- }}
- class="text-14-medium text-text-base min-w-0 truncate"
- displayClass="text-14-medium text-text-base min-w-0 truncate"
- editing={workspaceEditActive()}
- stopPropagation={false}
- openOnDblClick={false}
- />
- </Show>
- <Icon
- name={open() ? "chevron-down" : "chevron-right"}
- size="small"
- class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
- />
+ <Show
+ when={workspaceEditActive()}
+ fallback={
+ <Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
+ {header()}
+ </Collapsible.Trigger>
+ }
+ >
+ <div class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md">
+ {header()}
</div>
- </Collapsible.Trigger>
+ </Show>
<div
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
classList={{
@@ -1595,21 +1632,37 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" class="size-6 rounded-md" />
</Tooltip>
<DropdownMenu.Portal>
- <DropdownMenu.Content>
+ <DropdownMenu.Content
+ onCloseAutoFocus={(event) => {
+ if (!pendingRename()) return
+ event.preventDefault()
+ setPendingRename(false)
+ openEditor(`workspace:${props.directory}`, workspaceValue())
+ }}
+ >
<DropdownMenu.Item onSelect={() => navigate(`/${slug()}/session`)}>
<DropdownMenu.ItemLabel>New session</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
+ onSelect={() => {
+ setPendingRename(true)
+ setMenuOpen(false)
+ }}
+ >
+ <DropdownMenu.ItemLabel>Rename</DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
+ <DropdownMenu.Item
+ disabled={local()}
onSelect={() => dialog.show(() => <DialogResetWorkspace directory={props.directory} />)}
>
- <DropdownMenu.ItemLabel>Reset workspace</DropdownMenu.ItemLabel>
+ <DropdownMenu.ItemLabel>Reset</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
onSelect={() => dialog.show(() => <DialogDeleteWorkspace directory={props.directory} />)}
>
- <DropdownMenu.ItemLabel>Delete workspace</DropdownMenu.ItemLabel>
+ <DropdownMenu.ItemLabel>Delete</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
@@ -1681,7 +1734,7 @@ export default function Layout(props: ParentProps) {
const label = (directory: string) => {
const [data] = globalSync.child(directory)
const kind = directory === props.project.worktree ? "local" : "sandbox"
- const name = workspaceLabel(directory, data.vcs?.branch)
+ const name = workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}`
}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index d6ee121af..3b3002967 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -89,11 +89,26 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
length(): Promise<number>
}
- const WRITE_DEBOUNCE_MS = 250
+ const WRITE_DEBOUNCE_MS = 250
- const storeCache = new Map<string, Promise<StoreLike>>()
- const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
- const memoryCache = new Map<string, StoreLike>()
+ const storeCache = new Map<string, Promise<StoreLike>>()
+ const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
+ const memoryCache = new Map<string, StoreLike>()
+
+ const flushAll = async () => {
+ const apis = Array.from(apiCache.values())
+ await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
+ }
+
+ if ("addEventListener" in globalThis) {
+ const handleVisibility = () => {
+ if (document.visibilityState !== "hidden") return
+ void flushAll()
+ }
+
+ window.addEventListener("pagehide", () => void flushAll())
+ document.addEventListener("visibilitychange", handleVisibility)
+ }
const createMemoryStore = () => {
const data = new Map<string, string>()