feat: add gsd sessions subcommand for session picker

Add a new `gsd sessions` subcommand that lists all saved sessions for
the current directory and lets the user interactively pick one to resume.

Currently `gsd --continue` only resumes the most recent session, with no
way to access older conversations. This change adds:

- `gsd sessions` subcommand that calls SessionManager.list() to enumerate
  all sessions for the current working directory
- Interactive numbered list showing date, message count, session name (if
  set), and a preview of the first message
- Selection by number to resume any past session via SessionManager.open()
- Subcommand help text (`gsd sessions --help`)
- Help text entry in the main `gsd --help` output

The implementation uses only existing SessionManager APIs (list, open) -
no SDK changes required.
This commit is contained in:
sgodoy90 2026-03-16 15:27:10 -06:00
parent 915112ca1f
commit 72cef21876
2 changed files with 78 additions and 3 deletions

View file

@ -35,6 +35,8 @@ interface CliFlags {
appendSystemPrompt?: string
tools?: string[]
messages: string[]
/** Set by `gsd sessions` when the user picks a specific session to resume */
_selectedSessionPath?: string
}
function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
@ -115,6 +117,63 @@ if (cliFlags.messages[0] === 'update') {
process.exit(0)
}
// `gsd sessions` — list past sessions and pick one to resume
if (cliFlags.messages[0] === 'sessions') {
const cwd = process.cwd()
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`
const projectSessionsDir = join(sessionsDir, safePath)
process.stderr.write(chalk.dim(`Loading sessions for ${cwd}...\n`))
const sessions = await SessionManager.list(cwd, projectSessionsDir)
if (sessions.length === 0) {
process.stderr.write(chalk.yellow('No sessions found for this directory.\n'))
process.exit(0)
}
process.stderr.write(chalk.bold(`\n Sessions (${sessions.length}):\n\n`))
const maxShow = 20
const toShow = sessions.slice(0, maxShow)
for (let i = 0; i < toShow.length; i++) {
const s = toShow[i]
const date = s.modified.toLocaleString()
const msgs = s.messageCount
const name = s.name ? ` ${chalk.cyan(s.name)}` : ''
const preview = s.firstMessage
? s.firstMessage.replace(/\n/g, ' ').substring(0, 80)
: chalk.dim('(empty)')
const num = String(i + 1).padStart(3)
process.stderr.write(` ${chalk.bold(num)}. ${chalk.green(date)} ${chalk.dim(`(${msgs} msgs)`)}${name}\n`)
process.stderr.write(` ${chalk.dim(preview)}\n\n`)
}
if (sessions.length > maxShow) {
process.stderr.write(chalk.dim(` ... and ${sessions.length - maxShow} more\n\n`))
}
// Interactive selection
const readline = await import('node:readline')
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
const answer = await new Promise<string>((resolve) => {
rl.question(chalk.bold(' Enter session number to resume (or q to quit): '), resolve)
})
rl.close()
const choice = parseInt(answer, 10)
if (isNaN(choice) || choice < 1 || choice > toShow.length) {
process.stderr.write(chalk.dim('Cancelled.\n'))
process.exit(0)
}
const selected = toShow[choice - 1]
process.stderr.write(chalk.green(`\nResuming session from ${selected.modified.toLocaleString()}...\n\n`))
// Mark for the interactive session below to open this specific session
cliFlags.continue = true
cliFlags._selectedSessionPath = selected.path
}
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
// Provision local managed binaries first so Pi sees them without probing PATH.
@ -350,9 +409,11 @@ if (existsSync(sessionsDir)) {
}
}
const sessionManager = cliFlags.continue
? SessionManager.continueRecent(cwd, projectSessionsDir)
: SessionManager.create(cwd, projectSessionsDir)
const sessionManager = cliFlags._selectedSessionPath
? SessionManager.open(cliFlags._selectedSessionPath, projectSessionsDir)
: cliFlags.continue
? SessionManager.continueRecent(cwd, projectSessionsDir)
: SessionManager.create(cwd, projectSessionsDir)
exitIfManagedResourcesAreNewer(agentDir)
initResources(agentDir)

View file

@ -18,6 +18,19 @@ const SUBCOMMAND_HELP: Record<string, string> = {
'',
'Equivalent to: npm install -g gsd-pi@latest',
].join('\n'),
sessions: [
'Usage: gsd sessions',
'',
'List all saved sessions for the current directory and interactively',
'pick one to resume. Shows date, message count, and a preview of the',
'first message for each session.',
'',
'Sessions are stored per-directory, so you only see sessions that were',
'started from the current working directory.',
'',
'Compare with --continue (-c) which always resumes the most recent session.',
].join('\n'),
}
export function printHelp(version: string): void {
@ -37,6 +50,7 @@ export function printHelp(version: string): void {
process.stdout.write('\nSubcommands:\n')
process.stdout.write(' config Re-run the setup wizard\n')
process.stdout.write(' update Update GSD to the latest version\n')
process.stdout.write(' sessions List and resume a past session\n')
process.stdout.write('\nRun gsd <subcommand> --help for subcommand-specific help.\n')
}