Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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