opencode-patches/patch_archive_ui.py

259 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""Patch opencode frontend for archive/unarchive UI."""
BASE = '/mnt/NVME/Docker/opencode-build/opencode/packages'
# ── 1. layout.tsx ──────────────────────────────────────────────────────────────
layout = BASE + '/app/src/pages/layout.tsx'
c = open(layout).read()
# Add unarchiveSession after archiveSession (right after the closing brace)
ARCHIVE_END = ''' if (session.id === params.id) {
if (nextSession) {
navigate(`/${params.dir}/session/${nextSession.id}`)
} else {
navigate(`/${params.dir}/session`)
}
}
}'''
UNARCHIVE_FN = '''
async function unarchiveSession(session: Session) {
const [, setStore] = globalSync.child(session.directory)
await (globalSDK.client.session.update as unknown as Function)({
directory: session.directory,
sessionID: session.id,
time: { archived: null },
})
setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
}'''
assert c.count(ARCHIVE_END) == 1, f'ARCHIVE_END found {c.count(ARCHIVE_END)} times'
c = c.replace(ARCHIVE_END, ARCHIVE_END + UNARCHIVE_FN, 1)
# Add unarchiveSession to workspaceSidebarCtx
OLD_CTX1 = ' archiveSession,\n workspaceName,'
NEW_CTX1 = ' archiveSession,\n unarchiveSession,\n workspaceName,'
assert c.count(OLD_CTX1) == 1, f'OLD_CTX1 found {c.count(OLD_CTX1)} times'
c = c.replace(OLD_CTX1, NEW_CTX1, 1)
# Add to projectSidebarCtx.sessionProps
OLD_CTX2 = ' archiveSession,\n },\n }'
NEW_CTX2 = ' archiveSession,\n unarchiveSession,\n },\n }'
assert c.count(OLD_CTX2) == 1, f'OLD_CTX2 found {c.count(OLD_CTX2)} times'
c = c.replace(OLD_CTX2, NEW_CTX2, 1)
open(layout, 'w').write(c)
print('layout.tsx patched OK')
# ── 2. sidebar-workspace.tsx ───────────────────────────────────────────────────
sw = BASE + '/app/src/pages/layout/sidebar-workspace.tsx'
c = open(sw).read()
# Expand solid-js import
c = c.replace(
'import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"',
'import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js"',
1
)
# Add A to router import
c = c.replace(
'import { useNavigate, useParams } from "@solidjs/router"',
'import { A, useNavigate, useParams } from "@solidjs/router"',
1
)
# Add useGlobalSDK import
c = c.replace(
'import { useGlobalSync, useQueryOptions } from "@/context/global-sync"',
'import { useGlobalSync, useQueryOptions } from "@/context/global-sync"\nimport { useGlobalSDK } from "@/context/global-sdk"',
1
)
# Add sessionTitle import
c = c.replace(
'import { pathKey } from "@/utils/path-key"',
'import { pathKey } from "@/utils/path-key"\nimport { sessionTitle } from "@/utils/session-title"',
1
)
# Add unarchiveSession to WorkspaceSidebarContext type
c = c.replace(
' archiveSession: (session: Session) => Promise<void>\n workspaceName:',
' archiveSession: (session: Session) => Promise<void>\n unarchiveSession: (session: Session) => Promise<void>\n workspaceName:',
1
)
# Insert ArchivedSessionsSection component before WorkspaceSessionList
ARCHIVED_COMPONENT = '''const ArchivedSessionsSection = (props: {
directory: string
slug: Accessor<string>
unarchiveSession: (session: Session) => Promise<void>
language: ReturnType<typeof useLanguage>
}): JSX.Element => {
const globalSDK = useGlobalSDK()
const [open, setOpen] = createSignal(false)
const [archived, setArchived] = createSignal<Session[]>([])
const [loading, setLoading] = createSignal(false)
const load = async () => {
setLoading(true)
try {
const result = await (globalSDK.client.session.list as unknown as (
q: Record<string, unknown>,
) => Promise<{ data: Session[] }>)({
directory: props.directory,
archived: "true",
})
setArchived((result?.data ?? []).filter((s) => !!s.time?.archived))
} finally {
setLoading(false)
}
}
return (
<Collapsible
variant="ghost"
open={open()}
onOpenChange={(val) => {
setOpen(val)
if (val) void load()
}}
>
<Collapsible.Trigger class="flex items-center gap-1 w-full pl-2 py-1 rounded-md hover:bg-surface-raised-base-hover">
<Icon name="archive" size="small" class="text-icon-weak shrink-0" />
<span class="text-12-medium text-text-weak flex-1 text-left">Archived</span>
<Icon
name={open() ? "chevron-down" : "chevron-right"}
size="small"
class="text-icon-weak shrink-0 mr-1"
/>
</Collapsible.Trigger>
<Collapsible.Content>
<Show when={loading()}>
<SessionSkeleton count={2} />
</Show>
<Show when={!loading() && archived().length === 0 && open()}>
<div class="pl-6 py-1 text-12-regular text-text-weak italic">No archived sessions</div>
</Show>
<For each={archived()}>
{(session) => (
<div class="group/archived relative w-full min-w-0 rounded-md pl-6 pr-1 flex items-center hover:bg-surface-raised-base-hover">
<A
href={`/${props.slug()}/session/${session.id}`}
class="min-w-0 flex-1 py-1 text-14-regular text-text-weak truncate"
>
{sessionTitle(session.title)}
</A>
<div class="shrink-0 opacity-0 group-hover/archived:opacity-100 transition-opacity">
<Tooltip value="Unarchive" placement="top">
<IconButton
icon="archive"
variant="ghost"
class="size-6 rounded-md"
aria-label="Unarchive"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
void props.unarchiveSession(session).then(() => void load())
}}
/>
</Tooltip>
</div>
</div>
)}
</For>
</Collapsible.Content>
</Collapsible>
)
}
'''
WORKSPACE_SESSION_LIST_MARKER = 'const WorkspaceSessionList = ('
assert c.count(WORKSPACE_SESSION_LIST_MARKER) == 1
c = c.replace(WORKSPACE_SESSION_LIST_MARKER, ARCHIVED_COMPONENT + WORKSPACE_SESSION_LIST_MARKER, 1)
# Add ArchivedSessionsSection inside SortableWorkspace Collapsible.Content
OLD_SORTABLE_CONTENT = ''' <WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={showNew}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
</Collapsible.Content>'''
NEW_SORTABLE_CONTENT = ''' <WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={showNew}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
<ArchivedSessionsSection
directory={props.directory}
slug={slug}
unarchiveSession={props.ctx.unarchiveSession}
language={language}
/>
</Collapsible.Content>'''
assert c.count(OLD_SORTABLE_CONTENT) == 1, f'OLD_SORTABLE_CONTENT found {c.count(OLD_SORTABLE_CONTENT)} times'
c = c.replace(OLD_SORTABLE_CONTENT, NEW_SORTABLE_CONTENT, 1)
# Add ArchivedSessionsSection inside LocalWorkspace
OLD_LOCAL_CONTENT = ''' <WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={() => false}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
</div>'''
NEW_LOCAL_CONTENT = ''' <WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={() => false}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
<ArchivedSessionsSection
directory={props.project.worktree}
slug={slug}
unarchiveSession={props.ctx.unarchiveSession}
language={language}
/>
</div>'''
assert c.count(OLD_LOCAL_CONTENT) == 1, f'OLD_LOCAL_CONTENT found {c.count(OLD_LOCAL_CONTENT)} times'
c = c.replace(OLD_LOCAL_CONTENT, NEW_LOCAL_CONTENT, 1)
open(sw, 'w').write(c)
print('sidebar-workspace.tsx patched OK')
print('All frontend patches applied!')