From a8ad74aef3f8b6cd84c76855cbbb30f4fb472b86 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 2 Dec 2025 19:24:05 -0600 Subject: [PATCH] add basic session list command --- packages/opencode/src/cli/cmd/session.ts | 106 +++++++++++++++++++++++ packages/opencode/src/index.ts | 2 + 2 files changed, 108 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/session.ts diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts new file mode 100644 index 000000000..c8b5b0336 --- /dev/null +++ b/packages/opencode/src/cli/cmd/session.ts @@ -0,0 +1,106 @@ +import type { Argv } from "yargs" +import { cmd } from "./cmd" +import { Session } from "../../session" +import { bootstrap } from "../bootstrap" +import { UI } from "../ui" +import { Locale } from "../../util/locale" +import { EOL } from "os" + +export const SessionCommand = cmd({ + command: "session", + describe: "manage sessions", + builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(), + async handler() {}, +}) + +export const SessionListCommand = cmd({ + command: "list", + describe: "list sessions", + builder: (yargs: Argv) => { + return yargs + .option("max-count", { + alias: "n", + describe: "limit to N most recent sessions", + type: "number", + }) + .option("format", { + describe: "output format", + type: "string", + choices: ["table", "json"], + default: "table", + }) + }, + handler: async (args) => { + await bootstrap(process.cwd(), async () => { + const sessions = [] + for await (const session of Session.list()) { + if (!session.parentID) { + sessions.push(session) + } + } + + sessions.sort((a, b) => b.time.updated - a.time.updated) + + const limitedSessions = args.maxCount ? sessions.slice(0, args.maxCount) : sessions + + if (limitedSessions.length === 0) { + return + } + + let output: string + if (args.format === "json") { + output = formatSessionJSON(limitedSessions) + } else { + output = formatSessionTable(limitedSessions) + } + + const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table" + + if (shouldPaginate) { + const proc = Bun.spawn({ + cmd: ["less", "-R", "-S"], + stdin: "pipe", + stdout: "inherit", + stderr: "inherit", + }) + + proc.stdin.write(output) + proc.stdin.end() + await proc.exited + } else { + console.log(output) + } + }) + }, +}) + +function formatSessionTable(sessions: Session.Info[]): string { + const lines: string[] = [] + + const maxIdWidth = Math.max(20, ...sessions.map((s) => s.id.length)) + const maxTitleWidth = Math.max(25, ...sessions.map((s) => s.title.length)) + + const header = `Session ID${" ".repeat(maxIdWidth - 10)} Title${" ".repeat(maxTitleWidth - 5)} Updated` + lines.push(header) + lines.push("─".repeat(header.length)) + for (const session of sessions) { + const truncatedTitle = Locale.truncate(session.title, maxTitleWidth) + const timeStr = Locale.todayTimeOrDateTime(session.time.updated) + const line = `${session.id.padEnd(maxIdWidth)} ${truncatedTitle.padEnd(maxTitleWidth)} ${timeStr}` + lines.push(line) + } + + return lines.join(EOL) +} + +function formatSessionJSON(sessions: Session.Info[]): string { + const jsonData = sessions.map((session) => ({ + id: session.id, + title: session.title, + updated: session.time.updated, + created: session.time.created, + projectId: session.projectID, + directory: session.directory, + })) + return JSON.stringify(jsonData, null, 2) +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 5fb6f966c..5ddf68e10 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -25,6 +25,7 @@ import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" +import { SessionCommand } from "./cli/cmd/session" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -93,6 +94,7 @@ const cli = yargs(hideBin(process.argv)) .command(ImportCommand) .command(GithubCommand) .command(PrCommand) + .command(SessionCommand) .fail((msg) => { if ( msg.startsWith("Unknown argument") ||