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:
parent
915112ca1f
commit
72cef21876
2 changed files with 78 additions and 3 deletions
67
src/cli.ts
67
src/cli.ts
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue