Skip to content

Commit d5bf91c

Browse files
Apply PR #15250: feat(app): view archived sessions & unarchive
2 parents 745b752 + 76cda30 commit d5bf91c

File tree

20 files changed

+307
-5
lines changed

20 files changed

+307
-5
lines changed

packages/app/src/components/dialog-settings.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SettingsGeneral } from "./settings-general"
88
import { SettingsKeybinds } from "./settings-keybinds"
99
import { SettingsProviders } from "./settings-providers"
1010
import { SettingsModels } from "./settings-models"
11+
import { SettingsArchive } from "./settings-archive"
1112

1213
export const DialogSettings: Component = () => {
1314
const language = useLanguage()
@@ -47,6 +48,16 @@ export const DialogSettings: Component = () => {
4748
</Tabs.Trigger>
4849
</div>
4950
</div>
51+
52+
<div class="flex flex-col gap-1.5">
53+
<Tabs.SectionTitle>{language.t("settings.section.data")}</Tabs.SectionTitle>
54+
<div class="flex flex-col gap-1.5 w-full">
55+
<Tabs.Trigger value="archive">
56+
<Icon name="archive" />
57+
{language.t("settings.archive.title")}
58+
</Tabs.Trigger>
59+
</div>
60+
</div>
5061
</div>
5162
</div>
5263
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
@@ -67,6 +78,9 @@ export const DialogSettings: Component = () => {
6778
<Tabs.Content value="models" class="no-scrollbar">
6879
<SettingsModels />
6980
</Tabs.Content>
81+
<Tabs.Content value="archive" class="no-scrollbar">
82+
<SettingsArchive />
83+
</Tabs.Content>
7084
</Tabs>
7185
</Dialog>
7286
)
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { Button } from "@opencode-ai/ui/button"
2+
import { Icon } from "@opencode-ai/ui/icon"
3+
import { RadioGroup } from "@opencode-ai/ui/radio-group"
4+
import { getFilename } from "@opencode-ai/util/path"
5+
import { Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
6+
import { useParams } from "@solidjs/router"
7+
import { useGlobalSDK } from "@/context/global-sdk"
8+
import { useGlobalSync } from "@/context/global-sync"
9+
import { useLanguage } from "@/context/language"
10+
import { useLayout } from "@/context/layout"
11+
import { getRelativeTime } from "@/utils/time"
12+
import { decode64 } from "@/utils/base64"
13+
import type { Session } from "@opencode-ai/sdk/v2/client"
14+
import { SessionSkeleton } from "@/pages/layout/sidebar-items"
15+
16+
type FilterScope = "all" | "current"
17+
18+
type ScopeOption = { value: FilterScope; label: "settings.archive.scope.all" | "settings.archive.scope.current" }
19+
20+
const scopeOptions: ScopeOption[] = [
21+
{ value: "all", label: "settings.archive.scope.all" },
22+
{ value: "current", label: "settings.archive.scope.current" },
23+
]
24+
25+
export const SettingsArchive: Component = () => {
26+
const language = useLanguage()
27+
const globalSDK = useGlobalSDK()
28+
const globalSync = useGlobalSync()
29+
const layout = useLayout()
30+
const params = useParams()
31+
const [removedIds, setRemovedIds] = createSignal<Set<string>>(new Set())
32+
33+
const projects = createMemo(() => globalSync.data.project)
34+
const layoutProjects = createMemo(() => layout.projects.list())
35+
const hasMultipleProjects = createMemo(() => projects().length > 1)
36+
const homedir = createMemo(() => globalSync.data.path.home)
37+
38+
const defaultScope = () => (hasMultipleProjects() ? "current" : "all")
39+
const [filterScope, setFilterScope] = createSignal<FilterScope>(defaultScope())
40+
41+
const currentDirectory = createMemo(() => decode64(params.dir) ?? "")
42+
43+
const currentProject = createMemo(() => {
44+
const dir = currentDirectory()
45+
if (!dir) return null
46+
return layoutProjects().find((p) => p.worktree === dir || p.sandboxes?.includes(dir)) ?? null
47+
})
48+
49+
const filteredProjects = createMemo(() => {
50+
if (filterScope() === "current" && currentProject()) {
51+
return [currentProject()!]
52+
}
53+
return layoutProjects()
54+
})
55+
56+
const getSessionLabel = (session: Session) => {
57+
const directory = session.directory
58+
const home = homedir()
59+
const path = home ? directory.replace(home, "~") : directory
60+
61+
if (filterScope() === "current" && currentProject()) {
62+
const current = currentProject()
63+
const kind =
64+
current && directory === current.worktree
65+
? language.t("workspace.type.local")
66+
: language.t("workspace.type.sandbox")
67+
const [store] = globalSync.child(directory, { bootstrap: false })
68+
const name = store.vcs?.branch ?? getFilename(directory)
69+
return `${kind} : ${name || path}`
70+
}
71+
72+
return path
73+
}
74+
75+
const [archivedSessions] = createResource(
76+
() => ({ scope: filterScope(), projects: filteredProjects() }),
77+
async ({ projects }) => {
78+
const allSessions: Session[] = []
79+
for (const project of projects) {
80+
const directories = [project.worktree, ...(project.sandboxes ?? [])]
81+
for (const directory of directories) {
82+
const result = await globalSDK.client.experimental.session.list({ directory, archived: true })
83+
const sessions = result.data ?? []
84+
for (const session of sessions) {
85+
allSessions.push(session)
86+
}
87+
}
88+
}
89+
return allSessions.sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
90+
},
91+
{ initialValue: [] },
92+
)
93+
94+
const displayedSessions = () => {
95+
const sessions = archivedSessions() ?? []
96+
const removed = removedIds()
97+
return sessions.filter((s) => !removed.has(s.id))
98+
}
99+
100+
const currentScopeOption = () => scopeOptions.find((o) => o.value === filterScope())
101+
102+
const unarchiveSession = async (session: Session) => {
103+
setRemovedIds((prev) => new Set(prev).add(session.id))
104+
await globalSDK.client.session.update({
105+
directory: session.directory,
106+
sessionID: session.id,
107+
time: { archived: null as any },
108+
})
109+
}
110+
111+
const handleScopeChange = (option: ScopeOption | undefined) => {
112+
if (!option) return
113+
setRemovedIds(new Set<string>())
114+
setFilterScope(option.value)
115+
}
116+
117+
return (
118+
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
119+
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
120+
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
121+
<h2 class="text-16-medium text-text-strong">{language.t("settings.archive.title")}</h2>
122+
<p class="text-14-regular text-text-weak">{language.t("settings.archive.description")}</p>
123+
</div>
124+
</div>
125+
126+
<div class="flex flex-col gap-4 max-w-[720px]">
127+
<Show when={hasMultipleProjects()}>
128+
<RadioGroup
129+
options={scopeOptions}
130+
current={currentScopeOption() ?? undefined}
131+
value={(o) => o.value}
132+
size="small"
133+
label={(o) => language.t(o.label)}
134+
onSelect={handleScopeChange}
135+
/>
136+
</Show>
137+
<Show
138+
when={!archivedSessions.loading}
139+
fallback={
140+
<div class="min-h-[700px]">
141+
<SessionSkeleton count={4} />
142+
</div>
143+
}
144+
>
145+
<Show
146+
when={displayedSessions().length}
147+
fallback={
148+
<div class="min-h-[700px]">
149+
<div class="text-14-regular text-text-weak">{language.t("settings.archive.none")}</div>
150+
</div>
151+
}
152+
>
153+
<div class="min-h-[700px] flex flex-col gap-2">
154+
<For each={displayedSessions()}>
155+
{(session) => (
156+
<div class="flex items-center justify-between gap-4 px-3 py-1 rounded-md hover:bg-surface-raised-base-hover">
157+
<div class="flex items-center gap-x-3 grow min-w-0">
158+
<div class="flex items-center gap-2 min-w-0">
159+
<span class="text-14-regular text-text-strong truncate">{session.title}</span>
160+
<span class="text-14-regular text-text-weak truncate">{getSessionLabel(session)}</span>
161+
</div>
162+
</div>
163+
<div class="flex items-center gap-4 shrink-0">
164+
<Show when={session.time?.updated}>
165+
{(updated) => (
166+
<span class="text-12-regular text-text-weak whitespace-nowrap">
167+
{getRelativeTime(new Date(updated()).toISOString())}
168+
</span>
169+
)}
170+
</Show>
171+
<Button
172+
size="normal"
173+
variant="secondary"
174+
onClick={() => unarchiveSession(session)}
175+
>
176+
{language.t("common.unarchive")}
177+
</Button>
178+
</div>
179+
</div>
180+
)}
181+
</For>
182+
</div>
183+
</Show>
184+
</Show>
185+
</div>
186+
</div>
187+
)
188+
}

packages/app/src/i18n/ar.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,9 @@ export const dict = {
734734
"workspace.reset.archived.one": "ستتم أرشفة جلسة واحدة.",
735735
"workspace.reset.archived.many": "ستتم أرشفة {{count}} جلسات.",
736736
"workspace.reset.note": "سيؤدي هذا إلى إعادة تعيين مساحة العمل لتتطابق مع الفرع الافتراضي.",
737+
"settings.archive.title": "الجلسات المؤرشفة",
738+
"settings.archive.description": "استعادة الجلسات المؤرشفة لجعلها مرئية في الشريط الجانبي.",
739+
"settings.archive.none": "لا توجد جلسات مؤرشفة.",
740+
"settings.archive.scope.all": "جميع المشاريع",
741+
"settings.archive.scope.current": "المشروع الحالي",
737742
}

packages/app/src/i18n/br.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,4 +742,9 @@ export const dict = {
742742
"workspace.reset.archived.one": "1 sessão será arquivada.",
743743
"workspace.reset.archived.many": "{{count}} sessões serão arquivadas.",
744744
"workspace.reset.note": "Isso redefinirá o espaço de trabalho para corresponder ao branch padrão.",
745+
"settings.archive.title": "Sessões arquivadas",
746+
"settings.archive.description": "Restaure sessões arquivadas para torná-las visíveis na barra lateral.",
747+
"settings.archive.none": "Nenhuma sessão arquivada.",
748+
"settings.archive.scope.all": "Todos os projetos",
749+
"settings.archive.scope.current": "Projeto atual",
745750
}

packages/app/src/i18n/bs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,4 +819,9 @@ export const dict = {
819819
"workspace.reset.archived.one": "1 sesija će biti arhivirana.",
820820
"workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.",
821821
"workspace.reset.note": "Ovo će resetovati radni prostor da odgovara podrazumijevanoj grani.",
822+
"settings.archive.title": "Arhivirane sesije",
823+
"settings.archive.description": "Vrati arhivirane sesije da bi bile vidljive u bočnoj traci.",
824+
"settings.archive.none": "Nema arhiviranih sesija.",
825+
"settings.archive.scope.all": "Svi projekti",
826+
"settings.archive.scope.current": "Trenutni projekt",
822827
}

packages/app/src/i18n/da.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,4 +813,9 @@ export const dict = {
813813
"workspace.reset.archived.one": "1 session vil blive arkiveret.",
814814
"workspace.reset.archived.many": "{{count}} sessioner vil blive arkiveret.",
815815
"workspace.reset.note": "Dette vil nulstille arbejdsområdet til at matche hovedgrenen.",
816+
"settings.archive.title": "Arkiverede sessioner",
817+
"settings.archive.description": "Gendan arkiverede sessioner for at gøre dem synlige i sidebjælken.",
818+
"settings.archive.none": "Ingen arkiverede sessioner.",
819+
"settings.archive.scope.all": "Alle projekter",
820+
"settings.archive.scope.current": "Nuværende projekt",
816821
}

packages/app/src/i18n/de.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,4 +751,10 @@ export const dict = {
751751
"workspace.reset.archived.one": "1 Sitzung wird archiviert.",
752752
"workspace.reset.archived.many": "{{count}} Sitzungen werden archiviert.",
753753
"workspace.reset.note": "Dadurch wird der Arbeitsbereich auf den Standard-Branch zurückgesetzt.",
754+
755+
"settings.archive.title": "Archivierte Sitzungen",
756+
"settings.archive.description": "Archivierte Sitzungen wiederherstellen, um sie in der Seitenleiste anzuzeigen.",
757+
"settings.archive.none": "Keine archivierten Sitzungen.",
758+
"settings.archive.scope.all": "Alle Projekte",
759+
"settings.archive.scope.current": "Aktuelles Projekt",
754760
} satisfies Partial<Record<Keys, string>>

packages/app/src/i18n/en.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ export const dict = {
576576
"common.rename": "Rename",
577577
"common.reset": "Reset",
578578
"common.archive": "Archive",
579+
"common.unarchive": "Unarchive",
579580
"common.delete": "Delete",
580581
"common.close": "Close",
581582
"common.edit": "Edit",
@@ -599,6 +600,7 @@ export const dict = {
599600

600601
"settings.section.desktop": "Desktop",
601602
"settings.section.server": "Server",
603+
"settings.section.data": "Data",
602604
"settings.tab.general": "General",
603605
"settings.tab.shortcuts": "Shortcuts",
604606
"settings.desktop.section.wsl": "WSL",
@@ -828,4 +830,10 @@ export const dict = {
828830
"workspace.reset.archived.one": "1 session will be archived.",
829831
"workspace.reset.archived.many": "{{count}} sessions will be archived.",
830832
"workspace.reset.note": "This will reset the workspace to match the default branch.",
833+
834+
"settings.archive.title": "Archived Sessions",
835+
"settings.archive.description": "Restore archived sessions to make them visible in the sidebar.",
836+
"settings.archive.none": "No archived sessions.",
837+
"settings.archive.scope.all": "All projects",
838+
"settings.archive.scope.current": "Current project",
831839
}

packages/app/src/i18n/es.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,4 +825,10 @@ export const dict = {
825825
"workspace.reset.archived.one": "1 sesión será archivada.",
826826
"workspace.reset.archived.many": "{{count}} sesiones serán archivadas.",
827827
"workspace.reset.note": "Esto restablecerá el espacio de trabajo para coincidir con la rama predeterminada.",
828+
829+
"settings.archive.title": "Sesiones archivadas",
830+
"settings.archive.description": "Restaura las sesiones archivadas para hacerlas visibles en la barra lateral.",
831+
"settings.archive.none": "No hay sesiones archivadas.",
832+
"settings.archive.scope.all": "Todos los proyectos",
833+
"settings.archive.scope.current": "Proyecto actual",
828834
}

packages/app/src/i18n/fr.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,9 @@ export const dict = {
749749
"workspace.reset.archived.one": "1 session sera archivée.",
750750
"workspace.reset.archived.many": "{{count}} sessions seront archivées.",
751751
"workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.",
752+
"settings.archive.title": "Sessions archivées",
753+
"settings.archive.description": "Restaurez les sessions archivées pour les rendre visibles dans la barre latérale.",
754+
"settings.archive.none": "Aucune session archivée.",
755+
"settings.archive.scope.all": "Tous les Projets",
756+
"settings.archive.scope.current": "Projet actuel",
752757
}

0 commit comments

Comments
 (0)