Add archive UI patch: unarchive button + archived sessions section
This commit is contained in:
parent
a1c90c92d7
commit
209aa16965
1 changed files with 259 additions and 0 deletions
259
patch_archive_ui.py
Normal file
259
patch_archive_ui.py
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
#!/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!')
|
||||||
Loading…
Reference in a new issue