Skip to content

Commit a2face3

Browse files
committed
wip(app): session options
1 parent a219615 commit a2face3

File tree

1 file changed

+273
-15
lines changed

1 file changed

+273
-15
lines changed

packages/app/src/pages/session.tsx

Lines changed: 273 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
1616
import { Dynamic } from "solid-js/web"
1717
import { useLocal } from "@/context/local"
1818
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
19-
import { createStore } from "solid-js/store"
19+
import { createStore, produce } from "solid-js/store"
2020
import { PromptInput } from "@/components/prompt-input"
2121
import { SessionContextUsage } from "@/components/session-context-usage"
2222
import { IconButton } from "@opencode-ai/ui/icon-button"
2323
import { Button } from "@opencode-ai/ui/button"
2424
import { Icon } from "@opencode-ai/ui/icon"
2525
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
26+
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
27+
import { Dialog } from "@opencode-ai/ui/dialog"
28+
import { TextField } from "@opencode-ai/ui/text-field"
2629
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
2730
import { Tabs } from "@opencode-ai/ui/tabs"
2831
import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -436,6 +439,218 @@ export default function Page() {
436439
if (!id) return false
437440
return sync.session.history.loading(id)
438441
})
442+
443+
const errorMessage = (err: unknown) => {
444+
if (err && typeof err === "object" && "data" in err) {
445+
const data = (err as { data?: { message?: string } }).data
446+
if (data?.message) return data.message
447+
}
448+
if (err instanceof Error) return err.message
449+
return language.t("common.requestFailed")
450+
}
451+
452+
async function archiveSession(sessionID: string) {
453+
const session = sync.session.get(sessionID)
454+
if (!session) return
455+
456+
const sessions = sync.data.session ?? []
457+
const index = sessions.findIndex((s) => s.id === sessionID)
458+
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
459+
460+
await sdk.client.session
461+
.update({ sessionID, time: { archived: Date.now() } })
462+
.then(() => {
463+
sync.set(
464+
produce((draft) => {
465+
const index = draft.session.findIndex((s) => s.id === sessionID)
466+
if (index !== -1) draft.session.splice(index, 1)
467+
}),
468+
)
469+
470+
if (params.id !== sessionID) return
471+
if (session.parentID) {
472+
navigate(`/${params.dir}/session/${session.parentID}`)
473+
return
474+
}
475+
if (nextSession) {
476+
navigate(`/${params.dir}/session/${nextSession.id}`)
477+
return
478+
}
479+
navigate(`/${params.dir}/session`)
480+
})
481+
.catch((err) => {
482+
showToast({
483+
title: language.t("common.requestFailed"),
484+
description: errorMessage(err),
485+
})
486+
})
487+
}
488+
489+
async function deleteSession(sessionID: string) {
490+
const session = sync.session.get(sessionID)
491+
if (!session) return false
492+
493+
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
494+
const index = sessions.findIndex((s) => s.id === sessionID)
495+
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
496+
497+
const result = await sdk.client.session
498+
.delete({ sessionID })
499+
.then((x) => x.data)
500+
.catch((err) => {
501+
showToast({
502+
title: language.t("session.delete.failed.title"),
503+
description: errorMessage(err),
504+
})
505+
return false
506+
})
507+
508+
if (!result) return false
509+
510+
sync.set(
511+
produce((draft) => {
512+
const removed = new Set<string>([sessionID])
513+
514+
const byParent = new Map<string, string[]>()
515+
for (const item of draft.session) {
516+
const parentID = item.parentID
517+
if (!parentID) continue
518+
const existing = byParent.get(parentID)
519+
if (existing) {
520+
existing.push(item.id)
521+
continue
522+
}
523+
byParent.set(parentID, [item.id])
524+
}
525+
526+
const stack = [sessionID]
527+
while (stack.length) {
528+
const parentID = stack.pop()
529+
if (!parentID) continue
530+
531+
const children = byParent.get(parentID)
532+
if (!children) continue
533+
534+
for (const child of children) {
535+
if (removed.has(child)) continue
536+
removed.add(child)
537+
stack.push(child)
538+
}
539+
}
540+
541+
draft.session = draft.session.filter((s) => !removed.has(s.id))
542+
}),
543+
)
544+
545+
if (params.id !== sessionID) return true
546+
if (session.parentID) {
547+
navigate(`/${params.dir}/session/${session.parentID}`)
548+
return true
549+
}
550+
if (nextSession) {
551+
navigate(`/${params.dir}/session/${nextSession.id}`)
552+
return true
553+
}
554+
navigate(`/${params.dir}/session`)
555+
return true
556+
}
557+
558+
function DialogRenameSession(props: { sessionID: string }) {
559+
const [data, setData] = createStore({
560+
title: sync.session.get(props.sessionID)?.title ?? "",
561+
saving: false,
562+
})
563+
564+
const submit = (event: Event) => {
565+
event.preventDefault()
566+
if (data.saving) return
567+
568+
const title = data.title.trim()
569+
if (!title) {
570+
dialog.close()
571+
return
572+
}
573+
574+
const current = sync.session.get(props.sessionID)?.title ?? ""
575+
if (title === current) {
576+
dialog.close()
577+
return
578+
}
579+
580+
setData("saving", true)
581+
void sdk.client.session
582+
.update({ sessionID: props.sessionID, title })
583+
.then(() => {
584+
sync.set(
585+
produce((draft) => {
586+
const index = draft.session.findIndex((s) => s.id === props.sessionID)
587+
if (index !== -1) draft.session[index].title = title
588+
}),
589+
)
590+
dialog.close()
591+
})
592+
.catch((err) => {
593+
showToast({
594+
title: language.t("common.requestFailed"),
595+
description: errorMessage(err),
596+
})
597+
})
598+
.finally(() => {
599+
setData("saving", false)
600+
})
601+
}
602+
603+
return (
604+
<Dialog title={language.t("common.rename")} fit>
605+
<form onSubmit={submit} class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
606+
<TextField
607+
autofocus
608+
type="text"
609+
label={language.t("common.rename")}
610+
value={data.title}
611+
onChange={(value) => setData("title", value)}
612+
/>
613+
<div class="flex justify-end gap-2">
614+
<Button type="button" variant="ghost" size="large" disabled={data.saving} onClick={() => dialog.close()}>
615+
{language.t("common.cancel")}
616+
</Button>
617+
<Button type="submit" variant="primary" size="large" disabled={data.saving || !data.title.trim()}>
618+
{language.t("common.save")}
619+
</Button>
620+
</div>
621+
</form>
622+
</Dialog>
623+
)
624+
}
625+
626+
function DialogDeleteSession(props: { sessionID: string }) {
627+
const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
628+
const handleDelete = async () => {
629+
await deleteSession(props.sessionID)
630+
dialog.close()
631+
}
632+
633+
return (
634+
<Dialog title={language.t("session.delete.title")} fit>
635+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
636+
<div class="flex flex-col gap-1">
637+
<span class="text-14-regular text-text-strong">
638+
{language.t("session.delete.confirm", { name: title() })}
639+
</span>
640+
</div>
641+
<div class="flex justify-end gap-2">
642+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
643+
{language.t("common.cancel")}
644+
</Button>
645+
<Button variant="primary" size="large" onClick={handleDelete}>
646+
{language.t("session.delete.button")}
647+
</Button>
648+
</div>
649+
</div>
650+
</Dialog>
651+
)
652+
}
653+
439654
const emptyUserMessages: UserMessage[] = []
440655
const userMessages = createMemo(
441656
() => messages().filter((m) => m.role === "user") as UserMessage[],
@@ -1992,20 +2207,63 @@ export default function Page() {
19922207
centered(),
19932208
}}
19942209
>
1995-
<div class="h-10 flex items-center gap-1">
1996-
<Show when={info()?.parentID}>
1997-
<IconButton
1998-
tabIndex={-1}
1999-
icon="arrow-left"
2000-
variant="ghost"
2001-
onClick={() => {
2002-
navigate(`/${params.dir}/session/${info()?.parentID}`)
2003-
}}
2004-
aria-label={language.t("common.goBack")}
2005-
/>
2006-
</Show>
2007-
<Show when={info()?.title}>
2008-
<h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
2210+
<div class="h-10 w-full flex items-center justify-between gap-2">
2211+
<div class="flex items-center gap-1 min-w-0">
2212+
<Show when={info()?.parentID}>
2213+
<IconButton
2214+
tabIndex={-1}
2215+
icon="arrow-left"
2216+
variant="ghost"
2217+
onClick={() => {
2218+
navigate(`/${params.dir}/session/${info()?.parentID}`)
2219+
}}
2220+
aria-label={language.t("common.goBack")}
2221+
/>
2222+
</Show>
2223+
<Show when={info()?.title}>
2224+
<h1 class="text-16-medium text-text-strong truncate min-w-0">{info()?.title}</h1>
2225+
</Show>
2226+
</div>
2227+
<Show when={params.id}>
2228+
{(id) => (
2229+
<div class="shrink-0 flex items-center">
2230+
<DropdownMenu>
2231+
<Tooltip value={language.t("common.moreOptions")} placement="top">
2232+
<DropdownMenu.Trigger
2233+
as={IconButton}
2234+
icon="dot-grid"
2235+
variant="ghost"
2236+
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
2237+
aria-label={language.t("common.moreOptions")}
2238+
/>
2239+
</Tooltip>
2240+
<DropdownMenu.Portal>
2241+
<DropdownMenu.Content>
2242+
<DropdownMenu.Item
2243+
onSelect={() => dialog.show(() => <DialogRenameSession sessionID={id()} />)}
2244+
>
2245+
<DropdownMenu.ItemLabel>
2246+
{language.t("common.rename")}
2247+
</DropdownMenu.ItemLabel>
2248+
</DropdownMenu.Item>
2249+
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
2250+
<DropdownMenu.ItemLabel>
2251+
{language.t("common.archive")}
2252+
</DropdownMenu.ItemLabel>
2253+
</DropdownMenu.Item>
2254+
<DropdownMenu.Separator />
2255+
<DropdownMenu.Item
2256+
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
2257+
>
2258+
<DropdownMenu.ItemLabel>
2259+
{language.t("common.delete")}
2260+
</DropdownMenu.ItemLabel>
2261+
</DropdownMenu.Item>
2262+
</DropdownMenu.Content>
2263+
</DropdownMenu.Portal>
2264+
</DropdownMenu>
2265+
</div>
2266+
)}
20092267
</Show>
20102268
</div>
20112269
</div>

0 commit comments

Comments
 (0)