Skip to content
3 changes: 3 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const dict = {
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.archive": "Archive session",
"command.session.unarchive": "Unarchive session",

"command.palette": "Command palette",

Expand Down Expand Up @@ -440,6 +441,8 @@ export const dict = {
"toast.session.unshare.success.description": "Session unshared successfully!",
"toast.session.unshare.failed.title": "Failed to unshare session",
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
"toast.session.archive.success.title": "Session archived",
"toast.session.archive.success.description": "Use Undo to restore it.",

"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
Expand Down
81 changes: 73 additions & 8 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,14 @@ export default function Layout(props: ParentProps) {
return result
})

const selectedSession = createMemo(() => {
if (!params.dir || !params.id) return
const directory = decode64(params.dir)
if (!directory) return
const [store] = globalSync.child(directory, { bootstrap: false })
return store.session.find((s) => s.id === params.id)
})

type PrefetchQueue = {
inflight: Set<string>
pending: string[]
Expand Down Expand Up @@ -898,25 +906,71 @@ export default function Layout(props: ParentProps) {
const sessions = store.session ?? []
const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1]
const active = session.id === params.id

await globalSDK.client.session.update({
directory: session.directory,
sessionID: session.id,
time: { archived: Date.now() },
})
const archived = await globalSDK.client.session
.update({
directory: session.directory,
sessionID: session.id,
time: { archived: Date.now() },
})
.then(() => true)
.catch((err) => {
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err, language.t("common.requestFailed")),
})
return false
})
if (!archived) return
setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
if (session.id === params.id) {
if (active) {
if (nextSession) {
navigate(`/${params.dir}/session/${nextSession.id}`)
} else {
navigate(`/${params.dir}/session`)
}
}

showToast({
title: language.t("toast.session.archive.success.title"),
description: language.t("toast.session.archive.success.description"),
actions: [
{
label: language.t("command.session.undo"),
onClick: () => {
void unarchiveSession(session, session.time.updated ?? session.time.created)
.then(() => {
if (!active) return
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
})
.catch((err) => {
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err, language.t("common.requestFailed")),
})
})
},
},
{
label: language.t("common.dismiss"),
onClick: "dismiss",
},
],
})
}

async function unarchiveSession(session: Session, updated?: number) {
await globalSDK.client.session.update({
directory: session.directory,
sessionID: session.id,
time: { archived: null, updated },
})
}

command.register("layout", () => {
Expand Down Expand Up @@ -987,12 +1041,23 @@ export default function Layout(props: ParentProps) {
title: language.t("command.session.archive"),
category: language.t("command.category.session"),
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
disabled: !selectedSession() || !!selectedSession()?.time?.archived,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
const session = selectedSession()
if (session) archiveSession(session)
},
},
{
id: "session.unarchive",
title: language.t("command.session.unarchive"),
category: language.t("command.category.session"),
keybind: "mod+shift+u",
disabled: !selectedSession()?.time?.archived,
onSelect: () => {
const session = selectedSession()
if (session) unarchiveSession(session, session.time.updated ?? session.time.created)
},
},
{
id: "workspace.new",
title: language.t("workspace.new"),
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const dict = {
"desktop.menu.view.forward": "Forward",
"desktop.menu.view.previousSession": "Previous Session",
"desktop.menu.view.nextSession": "Next Session",
"desktop.menu.view.archiveSession": "Archive Session",
"desktop.menu.view.unarchiveSession": "Unarchive Session",
"desktop.menu.help.documentation": "OpenCode Documentation",
"desktop.menu.help.supportForum": "Support Forum",
"desktop.menu.help.shareFeedback": "Share Feedback",
Expand Down
10 changes: 10 additions & 0 deletions packages/desktop/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ export async function createMenu(trigger: (id: string) => void) {
text: t("desktop.menu.view.nextSession"),
accelerator: "Option+ArrowDown",
}),
await MenuItem.new({
action: () => trigger("session.archive"),
text: t("desktop.menu.view.archiveSession"),
accelerator: "Cmd+Shift+Backspace",
}),
await MenuItem.new({
action: () => trigger("session.unarchive"),
text: t("desktop.menu.view.unarchiveSession"),
accelerator: "Cmd+Shift+U",
}),
await PredefinedMenuItem.new({
item: "Separator",
}),
Expand Down
11 changes: 8 additions & 3 deletions packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ export const SessionRoutes = lazy(() =>
title: z.string().optional(),
time: z
.object({
archived: z.number().optional(),
archived: z.number().nullable().optional(),
updated: z.number().optional(),
})
.optional(),
}),
Expand All @@ -280,8 +281,12 @@ export const SessionRoutes = lazy(() =>
if (updates.title !== undefined) {
session = await Session.setTitle({ sessionID, title: updates.title })
}
if (updates.time?.archived !== undefined) {
session = await Session.setArchived({ sessionID, time: updates.time.archived })
if (updates.time && "archived" in updates.time) {
session = await Session.setArchived({
sessionID,
time: updates.time.archived ?? null,
updated: updates.time.updated,
})
}

return c.json(session)
Expand Down
14 changes: 7 additions & 7 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,16 +395,16 @@ export namespace Session {
export const setArchived = fn(
z.object({
sessionID: Identifier.schema("session"),
time: z.number().optional(),
time: z.number().nullable().optional(),
updated: z.number().optional(),
}),
async (input) => {
return Database.use((db) => {
const row = db
.update(SessionTable)
.set({ time_archived: input.time })
.where(eq(SessionTable.id, input.sessionID))
.returning()
.get()
const set =
input.updated === undefined
? { time_archived: input.time }
: { time_archived: input.time, time_updated: input.updated }
const row = db.update(SessionTable).set(set).where(eq(SessionTable.id, input.sessionID)).returning().get()
if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` })
const info = fromRow(row)
Database.effect(() => Bus.publish(Event.Updated, { info }))
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/util/which.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import whichPkg from "which"
import { createRequire } from "node:module"

const req = createRequire(import.meta.url)
const mod = req("which") as {
sync: (cmd: string, opts: { nothrow: boolean; path?: string; pathExt?: string }) => string | null
}

export function which(cmd: string, env?: NodeJS.ProcessEnv) {
const result = whichPkg.sync(cmd, {
const result = mod.sync(cmd, {
nothrow: true,
path: env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path,
pathExt: env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt,
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1430,7 +1430,8 @@ export class Session2 extends HeyApiClient {
workspace?: string
title?: string
time?: {
archived?: number
archived?: number | null
updated?: number
}
},
options?: Options<never, ThrowOnError>,
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2893,7 +2893,8 @@ export type SessionUpdateData = {
body?: {
title?: string
time?: {
archived?: number
archived?: number | null
updated?: number
}
}
path: {
Expand Down
Loading