mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 15:02:32 +00:00
Add --resume flag with session selector
- New SessionSelectorComponent to browse and select sessions - Lists sessions sorted by last modified date - Shows first message, created/modified dates, message count - Automatically truncates long messages and formats dates - Adds --resume/-r flag to CLI - Session selector integrates with main flow
This commit is contained in:
parent
7813e14492
commit
458702b3a7
3 changed files with 247 additions and 3 deletions
|
|
@ -1,11 +1,13 @@
|
|||
import { Agent, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-agent";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
import { codingTools } from "./tools/index.js";
|
||||
import { SessionSelectorComponent } from "./tui/session-selector.js";
|
||||
import { TuiRenderer } from "./tui/tui-renderer.js";
|
||||
|
||||
// Get version from package.json
|
||||
|
|
@ -20,6 +22,7 @@ interface Args {
|
|||
apiKey?: string;
|
||||
systemPrompt?: string;
|
||||
continue?: boolean;
|
||||
resume?: boolean;
|
||||
help?: boolean;
|
||||
messages: string[];
|
||||
}
|
||||
|
|
@ -36,6 +39,8 @@ function parseArgs(args: string[]): Args {
|
|||
result.help = true;
|
||||
} else if (arg === "--continue" || arg === "-c") {
|
||||
result.continue = true;
|
||||
} else if (arg === "--resume" || arg === "-r") {
|
||||
result.resume = true;
|
||||
} else if (arg === "--provider" && i + 1 < args.length) {
|
||||
result.provider = args[++i];
|
||||
} else if (arg === "--model" && i + 1 < args.length) {
|
||||
|
|
@ -64,6 +69,7 @@ ${chalk.bold("Options:")}
|
|||
--api-key <key> API key (defaults to env vars)
|
||||
--system-prompt <text> System prompt (default: coding assistant prompt)
|
||||
--continue, -c Continue previous session
|
||||
--resume, -r Select a session to resume
|
||||
--help, -h Show this help
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
|
|
@ -114,6 +120,30 @@ Guidelines:
|
|||
|
||||
Current directory: ${process.cwd()}`;
|
||||
|
||||
async function selectSession(sessionManager: SessionManager): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const ui = new TUI(new ProcessTerminal());
|
||||
let selectedPath: string | null = null;
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
sessionManager,
|
||||
(path: string) => {
|
||||
selectedPath = path;
|
||||
ui.stop();
|
||||
resolve(path);
|
||||
},
|
||||
() => {
|
||||
ui.stop();
|
||||
resolve(null);
|
||||
},
|
||||
);
|
||||
|
||||
ui.addChild(selector);
|
||||
ui.setFocus(selector.getSelectList());
|
||||
ui.start();
|
||||
});
|
||||
}
|
||||
|
||||
async function runInteractiveMode(agent: Agent, sessionManager: SessionManager, version: string): Promise<void> {
|
||||
const renderer = new TuiRenderer(agent, sessionManager, version);
|
||||
|
||||
|
|
@ -176,7 +206,18 @@ export async function main(args: string[]) {
|
|||
}
|
||||
|
||||
// Setup session manager
|
||||
const sessionManager = new SessionManager(parsed.continue);
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume);
|
||||
|
||||
// Handle --resume flag: show session selector
|
||||
if (parsed.resume) {
|
||||
const selectedSession = await selectSession(sessionManager);
|
||||
if (!selectedSession) {
|
||||
console.log(chalk.dim("No session selected"));
|
||||
return;
|
||||
}
|
||||
// Set the selected session as the active session
|
||||
sessionManager.setSessionFile(selectedSession);
|
||||
}
|
||||
|
||||
// Determine provider and model
|
||||
const provider = (parsed.provider || "anthropic") as any;
|
||||
|
|
@ -259,8 +300,8 @@ export async function main(args: string[]) {
|
|||
}),
|
||||
});
|
||||
|
||||
// Load previous messages if continuing
|
||||
if (parsed.continue) {
|
||||
// Load previous messages if continuing or resuming
|
||||
if (parsed.continue || parsed.resume) {
|
||||
const messages = sessionManager.loadMessages();
|
||||
if (messages.length > 0) {
|
||||
console.log(chalk.dim(`Loaded ${messages.length} messages from previous session`));
|
||||
|
|
|
|||
|
|
@ -242,4 +242,99 @@ export class SessionManager {
|
|||
getSessionFile(): string {
|
||||
return this.sessionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all sessions for the current directory with metadata
|
||||
*/
|
||||
loadAllSessions(): Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
}> {
|
||||
const sessions: Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const files = readdirSync(this.sessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => join(this.sessionDir, f));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const stats = statSync(file);
|
||||
const content = readFileSync(file, "utf8");
|
||||
const lines = content.trim().split("\n");
|
||||
|
||||
let sessionId = "";
|
||||
let created = stats.birthtime;
|
||||
let messageCount = 0;
|
||||
let firstMessage = "";
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
|
||||
// Extract session ID from first session entry
|
||||
if (entry.type === "session" && !sessionId) {
|
||||
sessionId = entry.id;
|
||||
created = new Date(entry.timestamp);
|
||||
}
|
||||
|
||||
// Count messages
|
||||
if (entry.type === "message") {
|
||||
messageCount++;
|
||||
|
||||
// Get first user message
|
||||
if (!firstMessage && entry.message.role === "user") {
|
||||
const textContent = entry.message.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join(" ");
|
||||
firstMessage = textContent || "";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
|
||||
sessions.push({
|
||||
path: file,
|
||||
id: sessionId || "unknown",
|
||||
created,
|
||||
modified: stats.mtime,
|
||||
messageCount,
|
||||
firstMessage: firstMessage || "(no messages)",
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip files that can't be read
|
||||
console.error(`Failed to read session file ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by modified date (most recent first)
|
||||
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
} catch (error) {
|
||||
console.error("Failed to load sessions:", error);
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session file to an existing session
|
||||
*/
|
||||
setSessionFile(path: string): void {
|
||||
this.sessionFile = path;
|
||||
this.loadSessionId();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
108
packages/coding-agent/src/tui/session-selector.ts
Normal file
108
packages/coding-agent/src/tui/session-selector.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { type Component, Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
|
||||
interface SessionItem {
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a session selector
|
||||
*/
|
||||
export class SessionSelectorComponent extends Container {
|
||||
private selectList: SelectList;
|
||||
|
||||
constructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {
|
||||
super();
|
||||
|
||||
// Load all sessions
|
||||
const sessions = sessionManager.loadAllSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Text(chalk.gray(" No previous sessions found"), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.selectList = new SelectList([], 0);
|
||||
this.addChild(this.selectList);
|
||||
|
||||
// Auto-cancel if no sessions
|
||||
setTimeout(() => onCancel(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Format sessions as select items
|
||||
const items: SelectItem[] = sessions.map((session) => {
|
||||
// Format dates
|
||||
const formatDate = (date: Date): string => {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays === 1) return "yesterday";
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
|
||||
// Fallback to date string
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// Truncate first message to single line
|
||||
const truncatedMessage = session.firstMessage.replace(/\n/g, " ").trim();
|
||||
|
||||
// Build description with metadata
|
||||
const created = formatDate(session.created);
|
||||
const modified = formatDate(session.modified);
|
||||
const msgCount = `${session.messageCount} msg${session.messageCount !== 1 ? "s" : ""}`;
|
||||
|
||||
const description = `${truncatedMessage}\n${chalk.dim(`Created: ${created} • Modified: ${modified} • ${msgCount}`)}`;
|
||||
|
||||
return {
|
||||
value: session.path,
|
||||
label: truncatedMessage.substring(0, 80),
|
||||
description,
|
||||
};
|
||||
});
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Text(chalk.bold("Select a session to resume:"), 1, 0));
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(items, Math.min(10, items.length));
|
||||
|
||||
this.selectList.onSelect = (item) => {
|
||||
onSelect(item.value);
|
||||
};
|
||||
|
||||
this.selectList.onCancel = () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
this.addChild(this.selectList);
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
getSelectList(): SelectList {
|
||||
return this.selectList;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue