Reviewed PR details and linked issues.
UnderstandProposed solution to rewrite corrupted session files.
BuildRan tests to confirm fix.
VerifyAdded changelog entry documenting fix.
EditYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/933
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
>gh pr view 933 --repo badlogic/pi-mono --json title,body,comments,commits,files{"body":"## Problem\n\nWhen `SessionManager.setSessionFile()` loads an existing file that is empty or has an invalid/missing header, it sets `flushed = true` even though `fileEntries` is empty. This causes subsequent messages to be appended **without a session header**, permanently corrupting the session file.\n\n## Solution\n\nCheck if `fileEntries` is empty after loading and, if so, start a fresh session while preserving the explicit file path.\n\n```typescript\nif (this.fileEntries.length === 0) {\n const explicitPath = this.sessionFile;\n this.newSession();\n this.sessionFile = explicitPath;\n return;\n}\n```\n\n## Testing\n\nAdded 4 new tests covering:\n- Loading an empty file creates a new session with valid header\n- Loading a file without valid header creates a new session \n- Writing after loading corrupted file produces valid session structure\n- Explicit file path is preserved when recovering from corruption\n\nAll existing tests continue to pass.\n\n## Evidence\n\nExample of corrupted session file (messages without header, orphaned parentIds):\nhttps://gist.github.com/armanddp/a62986a0e01480af0339b28c818eadfb\n\nFixes #932","comments":[{"id":"IC_kwDOPbFNk87iK_4M","author":{"login":"github-actions"},"authorAssociation":"CONTRIBUTOR","body":"Hi @armanddp, thanks for your interest in contributing!\n\nWe ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.\n\n**Next steps:**\n1. Open an issue describing what you want to change and why (keep it concise, write in your human voice, AI slop will be closed)\n2. Once a maintainer approves with `lgtm`, you'll be added to the approved contributors list\n3. Then you can submit your PR\n\nThis PR will be closed automatically. See https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md for more details.","createdAt":"2026-01-24T12:05:09Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/933#issuecomment-3794533900","viewerDidAuthor":false}],"commits":[{"authoredDate":"2026-01-24T12:04:27Z","authors":[{"email":"[REDACTED]","id":"","login":"","name":"Armand du Plessis (badgerbot)"}],"committedDate":"2026-01-24T12:04:27Z","messageBody":"When setSessionFile loads an existing file that is empty or has an\ninvalid/missing header, it now starts a fresh session instead of\nsetting flushed=true with empty fileEntries.\n\nThis prevents messages from being appended without a session header,\nwhich causes permanent session corruption with orphaned parentIds.\n\nFixes #932","messageHeadline":"fix: handle empty/corrupted session files in setSessionFile","oid":"e83aba79e637d2c58e7783e4cd99b9f6a6216175"}],"files":[{"path":"packages/coding-agent/src/core/session-manager.ts","additions":10,"deletions":0},{"path":"packages/coding-agent/test/session-manager/file-operations.test.ts","additions":89,"deletions":2}],"title":"fix: handle empty/corrupted session files in setSessionFile"}>gh issue view 932 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"## Problem\n\nWhen `SessionManager.setSessionFile()` loads an existing file that is empty or has an invalid/missing header, it still sets `flushed = true`. This causes subsequent messages to be appended **without a session header**, creating a permanently corrupted session file.\n\n## Root Cause\n\nIn `packages/coding-agent/src/core/session-manager.ts`, the `setSessionFile()` method:\n\n```typescript\nsetSessionFile(sessionFile: string): void {\n this.sessionFile = resolve(sessionFile);\n if (existsSync(this.sessionFile)) {\n this.fileEntries = loadEntriesFromFile(this.sessionFile);\n // ↑ Returns [] if file is empty or has no valid header\n \n const header = this.fileEntries.find((e) => e.type === \"session\");\n this.sessionId = header?.id ?? randomUUID(); // New UUID generated\n \n // ... migration ...\n \n this._buildIndex();\n this.flushed = true; // 🐛 BUG: Set even when fileEntries is empty!\n } else {\n // ...\n }\n}\n```\n\nThe `loadEntriesFromFile()` function correctly returns `[]` when the file is empty or corrupted:\n\n```typescript\n// Validate session header\nif (entries.length === 0) return entries;\nconst header = entries[0];\nif (header.type !== \"session\" || typeof (header as any).id !== \"string\") {\n return [];\n}\n```\n\nBut `setSessionFile()` does not check if `fileEntries` is empty before setting `flushed = true`.\n\n## Impact\n\n1. File exists but is empty/corrupted → `fileEntries = []`\n2. No header found → new `sessionId` generated \n3. `flushed = true` set anyway\n4. First message appends directly to file **without header**\n5. Result: Permanently broken session file with:\n - No session header\n - Orphaned `parentId` references\n - All conversation history effectively lost\n\n## Example Corrupted File\n\nhttps://gist.github.com/armanddp/a62986a0e01480af0339b28c818eadfb\n\n```jsonl\n{\"type\":\"message\",\"id\":\"3d596f44\",\"parentId\":\"588873d9\",...}\n{\"type\":\"message\",\"id\":\"7f933825\",\"parentId\":\"3d596f44\",...}\n```\n\n- First entry has `parentId: \"588873d9\"` pointing to non-existent entry\n- No session header present\n\n## Suggested Fix\n\nCheck if loaded entries are empty/invalid and treat as new session:\n\n```typescript\nsetSessionFile(sessionFile: string): void {\n this.sessionFile = resolve(sessionFile);\n if (existsSync(this.sessionFile)) {\n this.fileEntries = loadEntriesFromFile(this.sessionFile);\n \n // If file was empty/corrupted, start fresh\n if (this.fileEntries.length === 0) {\n const explicitPath = this.sessionFile;\n this.newSession();\n this.sessionFile = explicitPath;\n return;\n }\n \n const header = this.fileEntries.find((e) => e.type === \"session\") as SessionHeader | undefined;\n this.sessionId = header?.id ?? randomUUID();\n \n if (migrateToCurrentVersion(this.fileEntries)) {\n this._rewriteFile();\n }\n \n this._buildIndex();\n this.flushed = true;\n } else {\n const explicitPath = this.sessionFile;\n this.newSession();\n this.sessionFile = explicitPath;\n }\n}\n```\n\n## Environment\n\n- Discovered via Clawdbot (which embeds pi-coding-agent for session management)\n- Package: `@mariozechner/pi-coding-agent`","comments":[{"id":"IC_kwDOPbFNk87iOv8m","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"@armanddp Any idea how the corruption happened?","createdAt":"2026-01-24T21:11:32Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/932#issuecomment-3795517222","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87iO89r","author":{"login":"armanddp"},"authorAssociation":"NONE","body":"I don't - all that seems plausible was the clawdbot gateway was killed or restarted before the headers was written. That was the process host. ","createdAt":"2026-01-24T21:36:21Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/932#issuecomment-3795570539","viewerDidAuthor":false}],"labels":[],"state":"OPEN","title":"SessionManager.setSessionFile() corrupts session when loading empty/invalid file"}>gh pr diff 933 --repo badlogic/pi-monodiff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts
index 95e2d9db4..9d6bb241a 100644
--- a/packages/coding-agent/src/core/session-manager.ts
+++ b/packages/coding-agent/src/core/session-manager.ts
@@ -648,6 +648,16 @@ export class SessionManager {
this.sessionFile = resolve(sessionFile);
if (existsSync(this.sessionFile)) {
this.fileEntries = loadEntriesFromFile(this.sessionFile);
+
+ // If file was empty or corrupted (no valid header), start fresh to avoid
+ // appending messages without a session header (which breaks the session)
+ if (this.fileEntries.length === 0) {
+ const explicitPath = this.sessionFile;
+ this.newSession();
+ this.sessionFile = explicitPath;
+ return;
+ }
+
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
this.sessionId = header?.id ?? randomUUID();
diff --git a/packages/coding-agent/test/session-manager/file-operations.test.ts b/packages/coding-agent/test/session-manager/file-operations.test.ts
index 4b5985780..bf1cf56fa 100644
--- a/packages/coding-agent/test/session-manager/file-operations.test.ts
+++ b/packages/coding-agent/test/session-manager/file-operations.test.ts
@@ -1,8 +1,8 @@
-import { mkdirSync, rmSync, writeFileSync } from "fs";
+import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
-import { findMostRecentSession, loadEntriesFromFile } from "../../src/core/session-manager.js";
+import { findMostRecentSession, loadEntriesFromFile, SessionManager } from "../../src/core/session-manager.js";
describe("loadEntriesFromFile", () => {
let tempDir: string;
@@ -125,3 +125,90 @@ describe("findMostRecentSession", () => {
expect(findMostRecentSession(tempDir)).toBe(valid);
});
});
+
+describe("SessionManager.setSessionFile with corrupted files", () => {
+ let tempDir: string;
+
+ beforeEach(() => {
+ tempDir = join(tmpdir(), `session-test-${Date.now()}`);
+ mkdirSync(tempDir, { recursive: true });
+ });
+
+ afterEach(() => {
+ rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it("creates new session when loading empty file", () => {
+ const emptyFile = join(tempDir, "empty.jsonl");
+ writeFileSync(emptyFile, "");
+
+ const sm = SessionManager.open(emptyFile, tempDir);
+
+ // Should have created a new session with valid header
+ expect(sm.getSessionId()).toBeTruthy();
+ expect(sm.getHeader()).toBeTruthy();
+ expect(sm.getHeader()?.type).toBe("session");
+ });
+
+ it("creates new session when loading file without valid header", () => {
+ const noHeaderFile = join(tempDir, "no-header.jsonl");
+ // File with messages but no session header (corrupted state)
+ writeFileSync(
+ noHeaderFile,
+ '{"type":"message","id":"abc","parentId":"orphaned","timestamp":"2025-01-01T00:00:00Z","message":{"role":"assistant","content":"test"}}\n',
+ );
+
+ const sm = SessionManager.open(noHeaderFile, tempDir);
+
+ // Should have created a new session with valid header
+ expect(sm.getSessionId()).toBeTruthy();
+ expect(sm.getHeader()).toBeTruthy();
+ expect(sm.getHeader()?.type).toBe("session");
+ });
+
+ it("writes valid session header when saving after loading corrupted file", () => {
+ const corruptedFile = join(tempDir, "corrupted.jsonl");
+ writeFileSync(corruptedFile, "");
+
+ const sm = SessionManager.open(corruptedFile, tempDir);
+
+ // Add user and assistant messages to trigger persistence
+ sm.appendMessage({ role: "user", content: "test", timestamp: Date.now() });
+ sm.appendMessage({
+ role: "assistant",
+ content: [{ type: "text", text: "response" }],
+ timestamp: Date.now(),
+ api: "anthropic-messages",
+ provider: "anthropic",
+ model: "test",
+ usage: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ totalTokens: 0,
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
+ },
+ stopReason: "stop",
+ });
+
+ // Verify the file now has a valid session header
+ const content = readFileSync(corruptedFile, "utf-8");
+ const lines = content.trim().split("\n").filter(Boolean);
+
+ expect(lines.length).toBeGreaterThanOrEqual(3); // header + user + assistant
+ const header = JSON.parse(lines[0]);
+ expect(header.type).toBe("session");
+ expect(header.id).toBeTruthy();
+ });
+
+ it("preserves explicit session file path when recovering from corrupted file", () => {
+ const explicitPath = join(tempDir, "my-session.jsonl");
+ writeFileSync(explicitPath, "");
+
+ const sm = SessionManager.open(explicitPath, tempDir);
+
+ // The session file path should be preserved
+ expect(sm.getSessionFile()).toBe(explicitPath);
+ });
+});PR: https://github.com/badlogic/pi-mono/pull/933 Changelog:
Fixed session recovery when loading empty or corrupted session files ([#933](https://github.com/badlogic/pi-mono/pull/933) by [@armanddp](https://github.com/armanddp)) before merge.Good:
Bad:
Ugly:
Questions or Assumptions:
Change summary:
Tests:
what's the correct solution to this?
The recovery should ensure the existing file is rewritten so the first line is a valid session header. Otherwise the file stays corrupted and loadEntriesFromFile keeps returning [].
A correct solution is:
This makes the recovery durable and ensures subsequent loads see a valid header.
that's a terrible solution. if someone accidentially passes a wrong file to --session that file gets blown away
Understood. A safer approach is to avoid touching the existing file and instead create a new session file when the header is invalid. That preserves accidental input files. This means:
If you want a different behavior, tell me the policy you want for invalid files.
ok, i think we should:
pi --session test.jsonl doesn't immediately create the session file i think or would it? check
>rg -n "--session" packages/coding-agent/srcrg: unrecognized flag --session
Command exited with code 2>rg -n "session" packages/coding-agent/src/clipackages/coding-agent/src/cli/session-picker.ts:2: * TUI session selector for --resume flag
packages/coding-agent/src/cli/session-picker.ts:6:import type { SessionInfo, SessionListProgress } from "../core/session-manager.js";
packages/coding-agent/src/cli/session-picker.ts:7:import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
packages/coding-agent/src/cli/session-picker.ts:11:/** Show TUI session selector and return selected session path or null if cancelled */
packages/coding-agent/src/cli/args.ts:25: session?: string;
packages/coding-agent/src/cli/args.ts:26: sessionDir?: string;
packages/coding-agent/src/cli/args.ts:87: } else if (arg === "--no-session") {
packages/coding-agent/src/cli/args.ts:89: } else if (arg === "--session" && i + 1 < args.length) {
packages/coding-agent/src/cli/args.ts:90: result.session = args[++i];
packages/coding-agent/src/cli/args.ts:91: } else if (arg === "--session-dir" && i + 1 < args.length) {
packages/coding-agent/src/cli/args.ts:92: result.sessionDir = args[++i];
packages/coding-agent/src/cli/args.ts:196: --continue, -c Continue previous session
packages/coding-agent/src/cli/args.ts:197: --resume, -r Select a session to resume
packages/coding-agent/src/cli/args.ts:198: --session <path> Use specific session file
packages/coding-agent/src/cli/args.ts:199: --session-dir <dir> Directory for session storage and lookup
packages/coding-agent/src/cli/args.ts:200: --no-session Don't save session (ephemeral)
packages/coding-agent/src/cli/args.ts:215: --export <file> Export session file to HTML and exit
packages/coding-agent/src/cli/args.ts:239: # Continue previous session
packages/coding-agent/src/cli/args.ts:260: # Export a session file to HTML
packages/coding-agent/src/cli/args.ts:261: ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl
packages/coding-agent/src/cli/args.ts:262: ${APP_NAME} --export session.jsonl output.html
packages/coding-agent/src/cli/args.ts:288: PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://buildwithpi.ai/session/)>rg -n "sessionDir|session" packages/coding-agent/src/main.ts packages/coding-agent/src/modes -g"*.ts"packages/coding-agent/src/main.ts:609: await runRpcMode(session);
packages/coding-agent/src/main.ts:622: const mode = new InteractiveMode(session, {
packages/coding-agent/src/main.ts:632: await runPrintMode(session, {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:16:import type { AgentSession } from "../../core/agent-session.js";
packages/coding-agent/src/modes/rpc/rpc-mode.ts:44:export async function runRpcMode(session: AgentSession): Promise<never> {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:257: const extensionRunner = session.extensionRunner;
packages/coding-agent/src/modes/rpc/rpc-mode.ts:259: await session.bindExtensions({
packages/coding-agent/src/modes/rpc/rpc-mode.ts:262: waitForIdle: () => session.agent.waitForIdle(),
packages/coding-agent/src/modes/rpc/rpc-mode.ts:264: const success = await session.newSession({ parentSession: options?.parentSession });
packages/coding-agent/src/modes/rpc/rpc-mode.ts:266: await options.setup(session.sessionManager);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:271: const result = await session.fork(entryId);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:275: const result = await session.navigateTree(targetId, {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:294: session.subscribe((event) => {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:311: session
packages/coding-agent/src/modes/rpc/rpc-mode.ts:322: await session.steer(command.message);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:327: await session.followUp(command.message);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:332: await session.abort();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:336: case "new_session": {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:338: const cancelled = !(await session.newSession(options));
packages/coding-agent/src/modes/rpc/rpc-mode.ts:339: return success(id, "new_session", { cancelled });
packages/coding-agent/src/modes/rpc/rpc-mode.ts:348: model: session.model,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:349: thinkingLevel: session.thinkingLevel,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:350: isStreaming: session.isStreaming,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:351: isCompacting: session.isCompacting,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:352: steeringMode: session.steeringMode,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:353: followUpMode: session.followUpMode,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:354: sessionFile: session.sessionFile,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:355: sessionId: session.sessionId,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:356: autoCompactionEnabled: session.autoCompactionEnabled,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:357: messageCount: session.messages.length,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:358: pendingMessageCount: session.pendingMessageCount,
packages/coding-agent/src/modes/rpc/rpc-mode.ts:368: const models = await session.modelRegistry.getAvailable();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:373: await session.setModel(model);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:378: const result = await session.cycleModel();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:386: const models = await session.modelRegistry.getAvailable();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:395: session.setThinkingLevel(command.level);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:400: const level = session.cycleThinkingLevel();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:412: session.setSteeringMode(command.mode);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:417: session.setFollowUpMode(command.mode);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:426: const result = await session.compact(command.customInstructions);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:431: session.setAutoCompactionEnabled(command.enabled);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:440: session.setAutoRetryEnabled(command.enabled);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:445: session.abortRetry();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:454: const result = await session.executeBash(command.command);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:459: session.abortBash();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:467: case "get_session_stats": {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:468: const stats = session.getSessionStats();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:469: return success(id, "get_session_stats", stats);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:473: const path = await session.exportToHtml(command.outputPath);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:477: case "switch_session": {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:478: const cancelled = !(await session.switchSession(command.sessionPath));
packages/coding-agent/src/modes/rpc/rpc-mode.ts:479: return success(id, "switch_session", { cancelled });
packages/coding-agent/src/modes/rpc/rpc-mode.ts:483: const result = await session.fork(command.entryId);
packages/coding-agent/src/modes/rpc/rpc-mode.ts:488: const messages = session.getUserMessagesForForking();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:493: const text = session.getLastAssistantText();
packages/coding-agent/src/modes/rpc/rpc-mode.ts:502: return success(id, "get_messages", { messages: session.messages });
packages/coding-agent/src/modes/rpc/rpc-mode.ts:519: const currentRunner = session.extensionRunner;
packages/coding-agent/src/modes/rpc/rpc-mode.ts:520: if (currentRunner?.hasHandlers("session_shutdown")) {
packages/coding-agent/src/modes/rpc/rpc-mode.ts:521: await currentRunner.emit({ type: "session_shutdown" });
packages/coding-agent/src/modes/rpc/rpc-types.ts:10:import type { SessionStats } from "../../core/agent-session.js";
packages/coding-agent/src/modes/rpc/rpc-types.ts:24: | { id?: string; type: "new_session"; parentSession?: string }
packages/coding-agent/src/modes/rpc/rpc-types.ts:55: | { id?: string; type: "get_session_stats" }
packages/coding-agent/src/modes/rpc/rpc-types.ts:57: | { id?: string; type: "switch_session"; sessionPath: string }
packages/coding-agent/src/modes/rpc/rpc-types.ts:76: sessionFile?: string;
packages/coding-agent/src/modes/rpc/rpc-types.ts:77: sessionId: string;
packages/coding-agent/src/modes/rpc/rpc-types.ts:94: | { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
packages/coding-agent/src/modes/rpc/rpc-types.ts:149: | { id?: string; type: "response"; command: "get_session_stats"; success: true; data: SessionStats }
packages/coding-agent/src/modes/rpc/rpc-types.ts:151: | { id?: string; type: "response"; command: "switch_session"; success: true; data: { cancelled: boolean } }
packages/coding-agent/src/modes/rpc/rpc-client.ts:11:import type { SessionStats } from "../../core/agent-session.js";
packages/coding-agent/src/modes/rpc/rpc-client.ts:197: * Start a new session, optionally with parent tracking.
packages/coding-agent/src/modes/rpc/rpc-client.ts:198: * @param parentSession - Optional parent session path for lineage tracking
packages/coding-agent/src/modes/rpc/rpc-client.ts:199: * @returns Object with `cancelled: true` if an extension cancelled the new session
packages/coding-agent/src/modes/rpc/rpc-client.ts:202: const response = await this.send({ type: "new_session", parentSession });
packages/coding-agent/src/modes/rpc/rpc-client.ts:207: * Get current session state.
packages/coding-agent/src/modes/rpc/rpc-client.ts:272: * Compact session context.
packages/coding-agent/src/modes/rpc/rpc-client.ts:316: * Get session statistics.
packages/coding-agent/src/modes/rpc/rpc-client.ts:319: const response = await this.send({ type: "get_session_stats" });
packages/coding-agent/src/modes/rpc/rpc-client.ts:324: * Export session to HTML.
packages/coding-agent/src/modes/rpc/rpc-client.ts:332: * Switch to a different session file.
packages/coding-agent/src/modes/rpc/rpc-client.ts:335: async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
packages/coding-agent/src/modes/rpc/rpc-client.ts:336: const response = await this.send({ type: "switch_session", sessionPath });
packages/coding-agent/src/modes/rpc/rpc-client.ts:366: * Get all messages in the session.
packages/coding-agent/src/modes/print-mode.ts:10:import type { AgentSession } from "../core/agent-session.js";
packages/coding-agent/src/modes/print-mode.ts:30:export async function runPrintMode(session: AgentSession, options: PrintModeOptions): Promise<void> {
packages/coding-agent/src/modes/print-mode.ts:33: const header = session.sessionManager.getHeader();
packages/coding-agent/src/modes/print-mode.ts:39: const extensionRunner = session.extensionRunner;
packages/coding-agent/src/modes/print-mode.ts:41: await session.bindExtensions({
packages/coding-agent/src/modes/print-mode.ts:43: waitForIdle: () => session.agent.waitForIdle(),
packages/coding-agent/src/modes/print-mode.ts:45: const success = await session.newSession({ parentSession: options?.parentSession });
packages/coding-agent/src/modes/print-mode.ts:47: await options.setup(session.sessionManager);
packages/coding-agent/src/modes/print-mode.ts:52: const result = await session.fork(entryId);
packages/coding-agent/src/modes/print-mode.ts:56: const result = await session.navigateTree(targetId, {
packages/coding-agent/src/modes/print-mode.ts:71: // Always subscribe to enable session persistence via _handleAgentEvent
packages/coding-agent/src/modes/print-mode.ts:72: session.subscribe((event) => {
packages/coding-agent/src/modes/print-mode.ts:81: await session.prompt(initialMessage, { images: initialImages });
packages/coding-agent/src/modes/print-mode.ts:86: await session.prompt(message);
packages/coding-agent/src/modes/print-mode.ts:91: const state = session.state;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:55:import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:69:import { type SessionContext, SessionManager } from "../../core/session-manager.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:93:import { SessionSelectorComponent } from "./components/session-selector.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:136: /** Warning message if session model couldn't be restored */
packages/coding-agent/src/modes/interactive/interactive-mode.ts:149: private session: AgentSession;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:240: return this.session.agent;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:242: private get sessionManager() {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:243: return this.session.sessionManager;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:246: return this.session.settingsManager;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:250: session: AgentSession,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:253: this.session = session;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:268: this.footer = new FooterComponent(session, this.footerDataProvider);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:269: this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:275: setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:289: this.session.scopedModels.length > 0
packages/coding-agent/src/modes/interactive/interactive-mode.ts:290: ? this.session.scopedModels.map((s) => s.model)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:291: : this.session.modelRegistry.getAvailable();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:315: { name: "export", description: "Export session to HTML file" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:316: { name: "share", description: "Share session as a secret GitHub gist" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:318: { name: "name", description: "Set session display name" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:319: { name: "session", description: "Show session info and stats" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:323: { name: "tree", description: "Navigate session tree (switch branches)" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:326: { name: "new", description: "Start a new session" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:327: { name: "compact", description: "Manually compact the session context" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:328: { name: "resume", description: "Resume a different session" },
packages/coding-agent/src/modes/interactive/interactive-mode.ts:333: const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
packages/coding-agent/src/modes/interactive/interactive-mode.ts:339: const extensionCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map(
packages/coding-agent/src/modes/interactive/interactive-mode.ts:347: // Build skill commands from session.skills (if enabled)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:351: for (const skill of this.session.resourceLoader.getSkills().skills) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:374: // Load changelog (only show new entries, skip for resumed sessions)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:489: * Update terminal title with session name and cwd.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:493: const sessionName = this.sessionManager.getSessionName();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:494: if (sessionName) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:495: this.ui.terminal.setTitle(`π - ${sessionName} - ${cwdBasename}`);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:524: const modelsJsonError = this.session.modelRegistry.getError();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:536: await this.session.prompt(initialMessage, { images: initialImages });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:546: await this.session.prompt(message);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:558: await this.session.prompt(userInput);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:591: * Only shows new entries since last seen version, skips for resumed sessions.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:594: // Skip changelog for resumed/continued sessions (already have messages)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:595: if (this.session.state.messages.length > 0) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:868: const metadata = this.session.resourceLoader.getPathMetadata();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:872: const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:879: const skills = this.session.resourceLoader.getSkills().skills;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:891: const skillDiagnostics = this.session.resourceLoader.getSkills().diagnostics;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:898: const templates = this.session.promptTemplates;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:917: const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:936: const extensionErrors = this.session.resourceLoader.getExtensions().errors;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:943: const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
packages/coding-agent/src/modes/interactive/interactive-mode.ts:953: const loadedThemes = this.session.resourceLoader.getThemes().themes;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:966: const themeDiagnostics = this.session.resourceLoader.getThemes().diagnostics;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:978: const extensionRunner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:986: await this.session.bindExtensions({
packages/coding-agent/src/modes/interactive/interactive-mode.ts:989: waitForIdle: () => this.session.agent.waitForIdle(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:997: const success = await this.session.newSession({ parentSession: options?.parentSession });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1003: await options.setup(this.sessionManager);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1014: this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1020: const result = await this.session.fork(entryId);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1028: this.showStatus("Forked to new session");
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1033: const result = await this.session.navigateTree(targetId, {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1069: const tools = this.session.extensionRunner?.getAllRegisteredTools() ?? [];
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1086: sessionManager: this.sessionManager,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1087: modelRegistry: this.session.modelRegistry,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1088: model: this.session.model,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1089: isIdle: () => !this.session.isStreaming,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1090: abort: () => this.session.abort(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1091: hasPendingMessages: () => this.session.pendingMessageCount > 0,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1095: getContextUsage: () => this.session.getContextUsage(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1716: } else if (this.session.isBashRunning) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1717: this.session.abortBash();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1833: if (text === "/session") {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1910: if (this.session.isBashRunning) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1924: if (this.session.isCompacting) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1928: await this.session.prompt(text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1937: if (this.session.isStreaming) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1940: await this.session.prompt(text, { streamingBehavior: "steer" });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1958: this.unsubscribe = this.session.subscribe(async (event) => {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2056: const retryAttempt = this.session.retryAttempt;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2150: this.session.abortCompaction();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2207: this.session.abortRetry();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2299: const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2371: * Render session context to chat. Used for initial load and rebuild after compaction.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2372: * @param sessionContext Session context to render
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2377: sessionContext: SessionContext,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2387: for (const message of sessionContext.messages) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2407: const retryAttempt = this.session.retryAttempt;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2439: // Get aligned messages and entries from session context
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2440: const context = this.sessionManager.buildSessionContext();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2446: // Show compaction info if session was compacted
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2447: const allEntries = this.sessionManager.getEntries();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2466: const context = this.sessionManager.buildSessionContext();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2500: const extensionRunner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2501: if (extensionRunner?.hasHandlers("session_shutdown")) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2503: type: "session_shutdown",
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2542: if (this.session.isCompacting) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2546: await this.session.prompt(text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2555: if (this.session.isStreaming) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2558: await this.session.prompt(text, { streamingBehavior: "followUp" });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2581: const level = this.session.thinkingLevel || "off";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2588: const newLevel = this.session.cycleThinkingLevel();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2600: const result = await this.session.cycleModel(direction);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2602: const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2630: // Rebuild chat from session messages
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2738: * Combines session queue and compaction queue.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2743: ...this.session.getSteeringMessages(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2747: ...this.session.getFollowUpMessages(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2755: * Clears both session queue and compaction queue.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2758: const { steering, followUp } = this.session.clearQueue();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2823: const extensionRunner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2841: this.session.clearQueue();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2856: await this.session.prompt(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2858: await this.session.followUp(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2860: await this.session.steer(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2872: await this.session.prompt(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2883: await this.session.prompt(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2887: const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2894: await this.session.prompt(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2896: await this.session.followUp(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2898: await this.session.steer(message.text);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2942: autoCompact: this.session.autoCompactionEnabled,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2947: steeringMode: this.session.steeringMode,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2948: followUpMode: this.session.followUpMode,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2949: thinkingLevel: this.session.thinkingLevel,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2950: availableThinkingLevels: this.session.getAvailableThinkingLevels(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2962: this.session.setAutoCompactionEnabled(enabled);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2984: this.session.setSteeringMode(mode);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2987: this.session.setFollowUpMode(mode);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:2990: this.session.setThinkingLevel(level);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3059: await this.session.setModel(model);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3100: if (this.session.scopedModels.length > 0) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3101: return this.session.scopedModels.map((scoped) => scoped.model);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3104: this.session.modelRegistry.refresh();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3106: return await this.session.modelRegistry.getAvailable();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3123: this.session.model,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3125: this.session.modelRegistry,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3126: this.session.scopedModels,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3129: await this.session.setModel(model);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3151: this.session.modelRegistry.refresh();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3152: const allModels = this.session.modelRegistry.getAvailable();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3159: // Check if session has scoped models (from previous session-only changes or CLI --models)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3160: const sessionScopedModels = this.session.scopedModels;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3161: const hasSessionScope = sessionScopedModels.length > 0;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3163: // Build enabled model IDs from session state or settings
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3168: // Use current session's scoped models
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3169: for (const sm of sessionScopedModels) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3178: const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3185: // Track current enabled state (session-only until persisted)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3189: // Helper to update session's scoped models (session-only, no persist)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3192: // Use current session thinking level, not settings default
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3193: const currentThinkingLevel = this.session.thinkingLevel;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3194: const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3195: this.session.setScopedModels(
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3203: this.session.setScopedModels([]);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3268: const userMessages = this.session.getUserMessagesForForking();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3279: const result = await this.session.fork(entryId);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3291: this.showStatus("Branched to new session");
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3303: const tree = this.sessionManager.getTree();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3304: const realLeafId = this.sessionManager.getLeafId();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3309: const entry = this.sessionManager.getEntry(visibleLeafId);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3316: this.showStatus("No entries in session");
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3373: this.session.abortBranchSummary();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3387: const result = await this.session.navigateTree(entryId, {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3425: this.sessionManager.appendLabelChange(entryId, label);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3438: SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3440: async (sessionPath) => {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3442: await this.handleResumeSession(sessionPath);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3452: this.sessionManager.getSessionFile(),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3458: private async handleResumeSession(sessionPath: string): Promise<void> {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3473: // Switch session via AgentSession (emits extension session events)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3474: await this.session.switchSession(sessionPath);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3479: this.showStatus("Resumed session");
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3484: const providers = this.session.modelRegistry.authStorage.list();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3486: (p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth",
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3497: this.session.modelRegistry.authStorage,
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3509: this.session.modelRegistry.authStorage.logout(providerId);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3510: this.session.modelRegistry.refresh();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3562: await this.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3604: this.session.modelRegistry.refresh();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3621: if (this.session.isStreaming) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3625: if (this.session.isCompacting) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3650: await this.session.reload();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3651: setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3653: const runner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3660: const modelsJsonError = this.session.modelRegistry.getError();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3676: const filePath = await this.session.exportToHtml(outputPath);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3679: this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3697: const tmpFile = path.join(os.tmpdir(), "session.html");
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3699: await this.session.exportToHtml(tmpFile);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3701: this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3778: const text = this.session.getLastAssistantText();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3795: const currentName = this.sessionManager.getSessionName();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3806: this.sessionManager.appendSessionInfo(name);
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3814: const stats = this.session.getSessionStats();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3815: const sessionName = this.sessionManager.getSessionName();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3818: if (sessionName) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3819: info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3821: info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3822: info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:3981: const extensionRunner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4014: // New session via session (emits extension session events)
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4015: await this.session.newSession();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4026: this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4049: ...this.session.messages.map((msg) => JSON.stringify(msg)),
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4070: const extensionRunner = this.session.extensionRunner;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4088: if (this.session.isStreaming) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4106: // Record the result in session
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4107: this.session.recordBashResult(command, result, { excludeFromContext });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4114: const isDeferred = this.session.isStreaming;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4128: const result = await this.session.executeBash(
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4159: const entries = this.sessionManager.getEntries();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4181: this.session.abortCompaction();
packages/coding-agent/src/modes/interactive/interactive-mode.ts:4200: result = await this.session.compact(customInstructions);
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:79: /** Called when a model is toggled (session-only, no persist) */
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:94: * Changes are session-only until explicitly persisted with Ctrl+S.
packages/coding-agent/src/modes/interactive/components/user-message-selector.ts:6: id: string; // Entry ID in the session
packages/coding-agent/src/modes/interactive/components/session-selector.ts:16:import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js";
packages/coding-agent/src/modes/interactive/components/session-selector.ts:20:import { filterAndSortSessions, type SortMode } from "./session-selector-search.js";
packages/coding-agent/src/modes/interactive/components/session-selector.ts:138: const confirmHint = "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel";
packages/coding-agent/src/modes/interactive/components/session-selector.ts:164: * Custom session list component with multi-line items and search
packages/coding-agent/src/modes/interactive/components/session-selector.ts:176: public onSelect?: (sessionPath: string) => void;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:183: public onDeleteSession?: (sessionPath: string) => Promise<void>;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:185: private maxVisible: number = 5; // Max sessions visible (each session: message + metadata + optional path + blank)
packages/coding-agent/src/modes/interactive/components/session-selector.ts:197: constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:198: this.allSessions = sessions;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:199: this.filteredSessions = sessions;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:221: setSessions(sessions: SessionInfo[], showCwd: boolean): void {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:222: this.allSessions = sessions;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:241: // Prevent deleting current session
packages/coding-agent/src/modes/interactive/components/session-selector.ts:243: this.onError?.("Cannot delete the currently active session");
packages/coding-agent/src/modes/interactive/components/session-selector.ts:261: // "All" scope - no sessions anywhere that match filter
packages/coding-agent/src/modes/interactive/components/session-selector.ts:262: lines.push(theme.fg("muted", truncateToWidth(" No sessions found", width, "…")));
packages/coding-agent/src/modes/interactive/components/session-selector.ts:268: truncateToWidth(" No sessions in current folder. Press Tab to view all.", width, "…"),
packages/coding-agent/src/modes/interactive/components/session-selector.ts:282: // Render visible sessions (message + metadata + optional path + blank line)
packages/coding-agent/src/modes/interactive/components/session-selector.ts:284: const session = this.filteredSessions[i];
packages/coding-agent/src/modes/interactive/components/session-selector.ts:286: const isConfirmingDelete = session.path === this.confirmingDeletePath;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:288: // Use session name if set, otherwise first message
packages/coding-agent/src/modes/interactive/components/session-selector.ts:289: const hasName = !!session.name;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:290: const displayText = session.name ?? session.firstMessage;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:311: const modified = formatSessionDate(session.modified);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:312: const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:314: if (this.showCwd && session.cwd) {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:315: metadataParts.push(shortenPath(session.cwd));
packages/coding-agent/src/modes/interactive/components/session-selector.ts:326: const pathText = ` ${shortenPath(session.path)}`;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:332: lines.push(""); // Blank line between sessions
packages/coding-agent/src/modes/interactive/components/session-selector.ts:443: * Delete a session file, trying the `trash` CLI first, then falling back to unlink
packages/coding-agent/src/modes/interactive/components/session-selector.ts:446: sessionPath: string,
packages/coding-agent/src/modes/interactive/components/session-selector.ts:449: const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
packages/coding-agent/src/modes/interactive/components/session-selector.ts:466: if (trashResult.status === 0 || !existsSync(sessionPath)) {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:472: await unlink(sessionPath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:483: * Component that renders a session selector
packages/coding-agent/src/modes/interactive/components/session-selector.ts:486: private sessionList: SessionList;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:500: // Focusable implementation - propagate to sessionList for IME cursor positioning
packages/coding-agent/src/modes/interactive/components/session-selector.ts:507: this.sessionList.focused = value;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:513: onSelect: (sessionPath: string) => void,
packages/coding-agent/src/modes/interactive/components/session-selector.ts:533: // Create session list (starts empty, will be populated after load)
packages/coding-agent/src/modes/interactive/components/session-selector.ts:534: this.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:538: this.sessionList.onSelect = (sessionPath) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:540: onSelect(sessionPath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:542: this.sessionList.onCancel = () => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:546: this.sessionList.onExit = () => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:550: this.sessionList.onToggleScope = () => this.toggleScope();
packages/coding-agent/src/modes/interactive/components/session-selector.ts:551: this.sessionList.onToggleSort = () => this.toggleSortMode();
packages/coding-agent/src/modes/interactive/components/session-selector.ts:554: this.sessionList.onTogglePath = (showPath) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:558: this.sessionList.onDeleteConfirmationChange = (path) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:562: this.sessionList.onError = (msg) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:567: // Handle session deletion
packages/coding-agent/src/modes/interactive/components/session-selector.ts:568: this.sessionList.onDeleteSession = async (sessionPath: string) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:569: const result = await deleteSessionFile(sessionPath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:573: this.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:576: this.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:579: const sessions = this.scope === "all" ? (this.allSessions ?? []) : (this.currentSessions ?? []);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:581: this.sessionList.setSessions(sessions, showCwd);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:593: this.addChild(this.sessionList);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:599: // Start loading current sessions immediately
packages/coding-agent/src/modes/interactive/components/session-selector.ts:614: .then((sessions) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:615: this.currentSessions = sessions;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:621: this.sessionList.setSessions(sessions, false);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:631: this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:632: this.sessionList.setSessions([], false);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:640: this.sessionList.setSortMode(this.sortMode);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:651: this.sessionList.setSessions(this.allSessions, true);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:657: this.sessionList.setSessions([], true);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:671: .then((sessions) => {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:672: this.allSessions = sessions;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:679: this.sessionList.setSessions(sessions, true);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:682: if (sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {
packages/coding-agent/src/modes/interactive/components/session-selector.ts:694: this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:695: this.sessionList.setSessions([], true);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:702: this.sessionList.setSessions(this.currentSessions ?? [], false);
packages/coding-agent/src/modes/interactive/components/session-selector.ts:708: return this.sessionList;
packages/coding-agent/src/modes/interactive/components/footer.ts:3:import type { AgentSession } from "../../../core/agent-session.js";
packages/coding-agent/src/modes/interactive/components/footer.ts:32: * Computes token/context stats from session, gets git branch and extension statuses from provider.
packages/coding-agent/src/modes/interactive/components/footer.ts:38: private session: AgentSession,
packages/coding-agent/src/modes/interactive/components/footer.ts:63: const state = this.session.state;
packages/coding-agent/src/modes/interactive/components/footer.ts:65: // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
packages/coding-agent/src/modes/interactive/components/footer.ts:72: for (const entry of this.session.sessionManager.getEntries()) {
packages/coding-agent/src/modes/interactive/components/footer.ts:112: // Add session name if set
packages/coding-agent/src/modes/interactive/components/footer.ts:113: const sessionName = this.session.sessionManager.getSessionName();
packages/coding-agent/src/modes/interactive/components/footer.ts:114: if (sessionName) {
packages/coding-agent/src/modes/interactive/components/footer.ts:115: pwd = `${pwd} • ${sessionName}`;
packages/coding-agent/src/modes/interactive/components/footer.ts:138: const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts:2:import type { ParsedSkillBlock } from "../../../core/agent-session.js";
packages/coding-agent/src/modes/interactive/components/index.ts:21:export { SessionSelectorComponent } from "./session-selector.js";
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:2:import type { SessionInfo } from "../../../core/session-manager.js";
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:24:function getSessionSearchText(session: SessionInfo): string {
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:25: return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`;
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:105:export function matchSession(session: SessionInfo, parsed: ParsedSearchQuery): MatchResult {
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:106: const text = getSessionSearchText(session);
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:145:export function filterAndSortSessions(sessions: SessionInfo[], query: string, sortMode: SortMode): SessionInfo[] {
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:147: if (!trimmed) return sessions;
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:155: for (const s of sessions) {
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:163: const scored: { session: SessionInfo; score: number }[] = [];
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:164: for (const s of sessions) {
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:167: scored.push({ session: s, score: res.score });
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:172: return b.session.modified.getTime() - a.session.modified.getTime();
packages/coding-agent/src/modes/interactive/components/session-selector-search.ts:175: return scored.map((r) => r.session);
packages/coding-agent/src/modes/interactive/components/tree-selector.ts:13:import type { SessionTreeNode } from "../../../core/session-manager.js";
packages/coding-agent/src/modes/interactive/components/tree-selector.ts:945: * Component that renders a session tree selector for navigation
[Showing lines 49-464 of 464 (50.0KB limit). Full output: /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/pi-bash-26a03613e13f239c.log]>rg -n "createSessionManager" packages/coding-agent/srcpackages/coding-agent/src/main.ts:311:async function createSessionManager(parsed: Args, cwd: string): Promise<SessionManager | undefined> {
packages/coding-agent/src/main.ts:552: let sessionManager = await createSessionManager(parsed, cwd);pi --session test.jsonl does not create the file immediately.
Flow:
SessionManager.open() calls setSessionFile().newSession() runs and sessionFile is set to the explicit path, but nothing is written._persist() after an assistant message exists. _persist() skips until there is at least one assistant message, then appends the header and entries.So with the current code, the session file appears only after the first assistant response. If we implement the “truncate and add header” behavior for existing invalid files, that would be the only case where the file is rewritten immediately.
valid solution you think?
Yes, that policy is consistent and solves the corruption case. It still avoids creating new files when --session points to a nonexistent path, and only rewrites when the path exists but is invalid. It does mean --session can overwrite a non session file that already exists. That is the tradeoff you specified.
If you want me to implement, say the word.
implement
continue
I see the PR's proposed fix. The issue is that it calls newSession() but doesn't rewrite the file. Let me implement the correct solution: if file exists but has no valid session, truncate and write a fresh header.
The PR diff shows the proposed code, but the current main branch doesn't have it yet. Let me check the current state of setSessionFile on main:
Good, this is the current main branch code without the PR changes. Let me implement the correct fix:
Now let me update the test file to add a test that verifies the file is actually rewritten with a valid header:
Now let me run the tests to verify the implementation:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 429 files in 321ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 41ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.>cd /Users/badlogic/workspaces/pi-mono && npm test -- packages/coding-agent/test/session-manager/file-operations.test.ts 2>&1> pi-monorepo@0.0.3 test
> npm run test --workspaces --if-present packages/coding-agent/test/session-manager/file-operations.test.ts
> @mariozechner/pi-agent-core@0.49.3 test
> vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/agent
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-manager/file-operations.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/agent
npm error workspace @mariozechner/pi-agent-core@0.49.3
npm error location /Users/badlogic/workspaces/pi-mono/packages/agent
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
> @mariozechner/pi-ai@0.49.3 test
> vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/ai
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-manager/file-operations.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/ai
npm error workspace @mariozechner/pi-ai@0.49.3
npm error location /Users/badlogic/workspaces/pi-mono/packages/ai
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
> @mariozechner/pi-coding-agent@0.49.3 test
> vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-manager/file-operations.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error workspace @mariozechner/pi-coding-agent@0.49.3
npm error location /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-manager/file-operations.test.ts
> @mariozechner/pi-tui@0.49.3 test
> node --test --import tsx test/*.test.ts packages/coding-agent/test/session-manager/file-operations.test.ts
Result: null
Result: null
Result: {
items: [
{ value: '/.nofollow/', label: '.nofollow/' },
{ value: '/.resolve/', label: '.resolve/' },
{ value: '/.vol/', label: '.vol/' },
{ value: '/Applications/', label: 'Applications/' },
{ value: '/bin/', label: 'bin/' },
{ value: '/cores/', label: 'cores/' },
{ value: '/dev/', label: 'dev/' },
{ value: '/etc/', label: 'etc/' },
{ value: '/home/', label: 'home/' },
{ value: '/Library/', label: 'Library/' },
{ value: '/opt/', label: 'opt/' },
{ value: '/private/', label: 'private/' },
{ value: '/sbin/', label: 'sbin/' },
{ value: '/System/', label: 'System/' },
{ value: '/tmp/', label: 'tmp/' },
{ value: '/Users/', label: 'Users/' },
{ value: '/usr/', label: 'usr/' },
{ value: '/var/', label: 'var/' },
{ value: '/Volumes/', label: 'Volumes/' },
{ value: '/.file', label: '.file' },
{ value: '/.VolumeIcon.icns', label: '.VolumeIcon.icns' }
],
prefix: '/'
}
▶ CombinedAutocompleteProvider
▶ extractPathPrefix
✔ extracts / from 'hey /' when forced (16.60825ms)
✔ extracts /A from '/A' when forced (2.694917ms)
✔ does not trigger for slash commands (0.694083ms)
✔ triggers for absolute paths after slash command argument (2.18175ms)
✔ extractPathPrefix (22.775291ms)
▶ fd @ file suggestions
✔ returns all files and folders for empty @ query (16.578959ms)
✔ matches file with extension in query (8.963ms)
✔ filters are case insensitive (11.243625ms)
✔ ranks directories before files (14.079ms)
✔ returns nested file paths (17.499209ms)
✔ matches deeply nested paths (16.026458ms)
✔ matches directory in middle of path with --full-path (14.525083ms)
✔ fd @ file suggestions (99.266334ms)
✔ CombinedAutocompleteProvider (122.361083ms)
▶ Editor component
▶ Prompt history navigation
✔ does nothing on Up arrow when history is empty (7.489625ms)
✔ shows most recent history entry on Up arrow when editor is empty (1.916834ms)
✔ cycles through history entries on repeated Up arrow (1.244709ms)
✔ returns to empty editor on Down arrow after browsing history (0.452209ms)
✔ navigates forward through history with Down arrow (2.207084ms)
✔ exits history mode when typing a character (0.629ms)
✔ exits history mode on setText (3.513541ms)
✔ does not add empty strings to history (2.737042ms)
✔ does not add consecutive duplicates to history (0.502084ms)
✔ allows non-consecutive duplicates in history (3.147708ms)
✔ uses cursor movement instead of history when editor has content (0.54375ms)
✔ limits history to 100 entries (1.731167ms)
✔ allows cursor movement within multi-line history entry with Down (0.293584ms)
✔ allows cursor movement within multi-line history entry with Up (0.263375ms)
✔ navigates from multi-line entry back to newer via Down after cursor movement (0.283541ms)
✔ Prompt history navigation (27.673292ms)
▶ public state accessors
✔ returns cursor position (1.098875ms)
✔ returns lines as a defensive copy (0.236625ms)
✔ public state accessors (1.395625ms)
▶ Shift+Enter handling
✔ treats split VS Code Shift+Enter as a newline (0.23375ms)
✔ inserts a literal backslash when not followed by Enter (0.207ms)
✔ Shift+Enter handling (0.479875ms)
▶ Unicode text editing behavior
✔ inserts mixed ASCII, umlauts, and emojis as literal text (0.578208ms)
✔ deletes single-code-unit unicode characters (umlauts) with Backspace (0.345ms)
✔ deletes multi-code-unit emojis with single Backspace (0.409709ms)
✔ inserts characters at the correct position after cursor movement over umlauts (0.298917ms)
✔ moves cursor across multi-code-unit emojis with single arrow key (0.287792ms)
✔ preserves umlauts across line breaks (0.294417ms)
✔ replaces the entire document with unicode text via setText (paste simulation) (0.218709ms)
✔ moves cursor to document start on Ctrl+A and inserts at the beginning (0.25475ms)
✔ deletes words correctly with Ctrl+W and Alt+Backspace (0.48925ms)
✔ navigates words correctly with Ctrl+Left/Right (0.529417ms)
✔ Unicode text editing behavior (3.828541ms)
▶ Grapheme-aware text wrapping
✔ wraps lines correctly when text contains wide emojis (15.351959ms)
✔ wraps long text with emojis at correct positions (0.442834ms)
✔ wraps CJK characters correctly (each is 2 columns wide) (0.618666ms)
✔ handles mixed ASCII and wide characters in wrapping (0.2745ms)
✔ renders cursor correctly on wide characters (0.300208ms)
✔ does not exceed terminal width with emoji at wrap boundary (1.9065ms)
✔ shows cursor at end of line before wrap, wraps on next char (4.347833ms)
✔ Grapheme-aware text wrapping (23.456959ms)
▶ Word wrapping
✔ wraps at word boundaries instead of mid-word (4.198209ms)
✔ does not start lines with leading whitespace after word wrap (0.634417ms)
✔ breaks long words (URLs) at character level (0.466584ms)
✔ preserves multiple spaces within words on same line (0.324625ms)
✔ handles empty string (0.219458ms)
✔ handles single word that fits exactly (0.225709ms)
✔ wraps word to next line when it ends exactly at terminal width (0.054416ms)
✔ keeps whitespace at terminal width boundary on same line (0.045875ms)
✔ handles unbreakable word filling width exactly followed by space (0.042333ms)
✔ wraps word to next line when it fits width but not remaining space (0.0435ms)
✔ keeps word with multi-space and following word together when they fit (0.046833ms)
✔ keeps word with multi-space and following word when they fill width exactly (0.05725ms)
✔ splits when word plus multi-space plus word exceeds width (0.05525ms)
✔ breaks long whitespace at line boundary (0.056541ms)
✔ breaks long whitespace at line boundary 2 (0.072208ms)
✔ breaks whitespace spanning full lines (0.071625ms)
✔ Word wrapping (6.8525ms)
▶ Kill ring
✔ Ctrl+W saves deleted text to kill ring and Ctrl+Y yanks it (0.432958ms)
✔ Ctrl+U saves deleted text to kill ring (0.345291ms)
✔ Ctrl+K saves deleted text to kill ring (0.242208ms)
✔ Ctrl+Y does nothing when kill ring is empty (0.186541ms)
✔ Alt+Y cycles through kill ring after Ctrl+Y (0.3125ms)
✔ Alt+Y does nothing if not preceded by yank (0.225167ms)
✔ Alt+Y does nothing if kill ring has ≤1 entry (0.20875ms)
✔ consecutive Ctrl+W accumulates into one kill ring entry (0.219166ms)
✔ Ctrl+U accumulates multiline deletes including newlines (8.870083ms)
✔ backward deletions prepend, forward deletions append during accumulation (0.409333ms)
✔ non-delete actions break kill accumulation (0.280625ms)
✔ non-yank actions break Alt+Y chain (0.237084ms)
✔ kill ring rotation persists after cycling (0.24425ms)
✔ consecutive deletions across lines coalesce into one entry (0.318959ms)
✔ Ctrl+K at line end deletes newline and coalesces (0.569333ms)
✔ handles yank in middle of text (0.323042ms)
✔ handles yank-pop in middle of text (0.313875ms)
✔ multiline yank and yank-pop in middle of text (0.319958ms)
✔ Alt+D deletes word forward and saves to kill ring (0.500083ms)
✔ Alt+D at end of line deletes newline (0.266ms)
✔ Kill ring (15.120875ms)
▶ Undo
✔ does nothing when undo stack is empty (0.313959ms)
✔ coalesces consecutive word characters into one undo unit (0.609416ms)
✔ undoes spaces one at a time (0.345791ms)
✔ undoes newlines and signals next word to capture state (0.348917ms)
✔ undoes backspace (0.269833ms)
✔ undoes forward delete (0.353708ms)
✔ undoes Ctrl+W (delete word backward) (0.348459ms)
✔ undoes Ctrl+K (delete to line end) (0.49725ms)
✔ undoes Ctrl+U (delete to line start) (0.465542ms)
✔ undoes yank (0.347333ms)
✔ undoes single-line paste atomically (0.41825ms)
✔ undoes multi-line paste atomically (0.339416ms)
✔ undoes insertTextAtCursor atomically (0.334834ms)
✔ undoes setText to empty string (0.338375ms)
✔ clears undo stack on submit (0.310333ms)
✔ exits history browsing mode on undo (0.297167ms)
✔ undo restores to pre-history state even after multiple history navigations (0.351084ms)
✔ cursor movement starts new undo unit (0.467208ms)
✔ no-op delete operations do not push undo snapshots (0.2725ms)
✔ undoes autocomplete (0.429167ms)
✔ Undo (7.730708ms)
✔ Editor component (87.020209ms)
▶ fuzzyMatch
✔ empty query matches everything with score 0 (0.681791ms)
✔ query longer than text does not match (0.119334ms)
✔ exact match has good score (0.122792ms)
✔ characters must appear in order (0.096917ms)
✔ case insensitive matching (0.072708ms)
✔ consecutive matches score better than scattered matches (0.068208ms)
✔ word boundary matches score better (0.053333ms)
✔ matches swapped alpha numeric tokens (0.070417ms)
✔ fuzzyMatch (1.902583ms)
▶ fuzzyFilter
✔ empty query returns all items unchanged (0.706542ms)
✔ filters out non-matching items (0.145625ms)
✔ sorts results by match quality (0.111584ms)
✔ works with custom getText function (0.074833ms)
✔ fuzzyFilter (1.165458ms)
▶ Input component
✔ treats split VS Code Shift+Enter as submit (1.937542ms)
✔ inserts a literal backslash when not followed by Enter (0.819ms)
✔ Input component (4.386208ms)
▶ matchesKey
▶ Kitty protocol with alternate keys (non-Latin layouts)
✔ should match Ctrl+c when pressing Ctrl+С (Cyrillic) with base layout key (1.007792ms)
✔ should match Ctrl+d when pressing Ctrl+В (Cyrillic) with base layout key (0.131292ms)
✔ should match Ctrl+z when pressing Ctrl+Я (Cyrillic) with base layout key (0.05925ms)
✔ should match Ctrl+Shift+p with base layout key (0.078833ms)
✔ should still match direct codepoint when no base layout key (0.060708ms)
✔ should handle shifted key in format (0.059791ms)
✔ should handle event type in format (0.062584ms)
✔ should handle full format with shifted key, base key, and event type (0.053458ms)
✔ should not match wrong key even with base layout (0.064792ms)
✔ should not match wrong modifiers even with base layout (0.095291ms)
✔ Kitty protocol with alternate keys (non-Latin layouts) (2.294583ms)
▶ Legacy key matching
✔ should match legacy Ctrl+c (1.031042ms)
✔ should match legacy Ctrl+d (0.059542ms)
✔ should match escape key (0.05975ms)
✔ should match legacy linefeed as enter (0.238542ms)
✔ should treat linefeed as shift+enter when kitty active (0.173208ms)
✔ should parse ctrl+space (0.040958ms)
✔ should match legacy Ctrl+symbol (0.046583ms)
✔ should match legacy Ctrl+Alt+symbol (0.046583ms)
✔ should parse legacy alt-prefixed sequences when kitty inactive (0.096833ms)
✔ should match arrow keys (0.0465ms)
✔ should match SS3 arrows and home/end (0.042458ms)
✔ should match legacy function keys and clear (0.052375ms)
✔ should match alt+arrows (0.036291ms)
✔ should match rxvt modifier sequences (0.051041ms)
✔ Legacy key matching (2.210625ms)
✔ matchesKey (4.965334ms)
▶ parseKey
▶ Kitty protocol with alternate keys
✔ should return Latin key name when base layout key is present (0.068125ms)
✔ should return key name from codepoint when no base layout (0.03425ms)
✔ Kitty protocol with alternate keys (0.141625ms)
▶ Legacy key parsing
✔ should parse legacy Ctrl+letter (0.060959ms)
✔ should parse special keys (1.093ms)
✔ should parse arrow keys (0.064208ms)
✔ should parse SS3 arrows and home/end (0.03975ms)
✔ should parse legacy function and modifier sequences (0.056834ms)
✔ should parse double bracket pageUp (0.033209ms)
✔ Legacy key parsing (1.430458ms)
✔ parseKey (1.903083ms)
▶ Markdown component
▶ Nested lists
✔ should render simple nested list (32.252625ms)
✔ should render deeply nested list (0.417584ms)
✔ should render ordered nested list (0.651666ms)
✔ should render mixed ordered and unordered nested lists (0.33925ms)
✔ should maintain numbering when code blocks are not indented (LLM output) (0.46875ms)
✔ Nested lists (35.489ms)
▶ Tables
✔ should render simple table (1.753084ms)
✔ should render table with alignment (10.706958ms)
✔ should handle tables with varying column widths (17.424458ms)
✔ should wrap table cells when table exceeds available width (5.521125ms)
✔ should wrap long cell content to multiple lines (3.81575ms)
✔ should wrap long unbroken tokens inside table cells (not only at line start) (4.8565ms)
✔ should wrap styled inline code inside table cells without breaking borders (0.32375ms)
✔ should handle extremely narrow width gracefully (0.151792ms)
✔ should render table correctly when it fits naturally (0.150333ms)
✔ should respect paddingX when calculating table width (1.755333ms)
✔ Tables (57.582ms)
▶ Combined features
✔ should render lists and tables together (0.594083ms)
✔ Combined features (0.640166ms)
▶ Pre-styled text (thinking traces)
✔ should preserve gray italic styling after inline code (1.218791ms)
✔ should preserve gray italic styling after bold text (5.084125ms)
✔ should not leak styles into following lines when rendered in TUI (9.234458ms)
✔ Pre-styled text (thinking traces) (15.647833ms)
▶ Spacing after code blocks
✔ should have only one blank line between code block and following paragraph (0.210458ms)
✔ Spacing after code blocks (0.251625ms)
▶ Spacing after dividers
✔ should have only one blank line between divider and following paragraph (0.171667ms)
✔ Spacing after dividers (0.195625ms)
▶ Spacing after headings
✔ should have only one blank line between heading and following paragraph (0.140625ms)
✔ Spacing after headings (0.162584ms)
▶ Spacing after blockquotes
✔ should have only one blank line between blockquote and following paragraph (0.251208ms)
✔ Spacing after blockquotes (0.273334ms)
▶ Links
✔ should not duplicate URL for autolinked emails (0.108167ms)
✔ should not duplicate URL for bare URLs (0.132125ms)
✔ should show URL for explicit markdown links with different text (0.171125ms)
✔ should show URL for explicit mailto links with different text (0.076916ms)
✔ Links (0.536125ms)
▶ HTML-like tags in text
✔ should render content with HTML-like tags as text (0.152834ms)
✔ should render HTML tags in code blocks correctly (0.070417ms)
✔ HTML-like tags in text (0.257875ms)
✔ Markdown component (112.045875ms)
▶ TUI overlay options
▶ width overflow protection
✔ should truncate overlay lines that exceed declared width (9.13675ms)
✔ should handle overlay with complex ANSI sequences without crashing (21.337167ms)
✔ should handle overlay composited on styled base content (7.806167ms)
✔ should handle wide characters at overlay boundary (6.558875ms)
✔ should handle overlay positioned at terminal edge (1.644208ms)
✔ should handle overlay on base content with OSC sequences (1.72175ms)
✔ width overflow protection (48.894791ms)
▶ width percentage
✔ should render overlay at percentage of terminal width (1.294042ms)
✔ should respect minWidth when widthPercent results in smaller width (1.382375ms)
✔ width percentage (2.824542ms)
▶ anchor positioning
✔ should position overlay at top-left (3.028375ms)
✔ should position overlay at bottom-right (1.63275ms)
✔ should position overlay at top-center (2.589375ms)
✔ anchor positioning (7.401042ms)
▶ margin
✔ should clamp negative margins to zero (3.979833ms)
✔ should respect margin as number (1.718833ms)
✔ should respect margin object (2.829666ms)
✔ margin (8.674834ms)
▶ offset
✔ should apply offsetX and offsetY from anchor position (3.800958ms)
✔ offset (3.947917ms)
▶ percentage positioning
✔ should position with rowPercent and colPercent (5.296292ms)
✔ rowPercent 0 should position at top (1.048625ms)
✔ rowPercent 100 should position at bottom (1.123125ms)
✔ percentage positioning (7.643166ms)
▶ maxHeight
✔ should truncate overlay to maxHeight (1.677583ms)
✔ should truncate overlay to maxHeightPercent (2.304625ms)
✔ maxHeight (4.087917ms)
▶ absolute positioning
✔ row and col should override anchor (3.156083ms)
✔ absolute positioning (3.232667ms)
▶ stacked overlays
✔ should render multiple overlays with later ones on top (1.083083ms)
✔ should handle overlays at different positions without interference (2.081416ms)
✔ should properly hide overlays in stack order (2.90725ms)
✔ stacked overlays (6.205958ms)
✔ TUI overlay options (93.447708ms)
Terminal rows: 24
Content lines: 3
Overlay visible: true
▶ TUI overlay with short content
✔ should render overlay when content is shorter than terminal height (17.952666ms)
✔ TUI overlay with short content (18.523875ms)
▶ SelectList
✔ normalizes multiline descriptions to single line (3.104834ms)
✔ SelectList (4.282041ms)
▶ StdinBuffer
▶ Regular Characters
✔ should pass through regular characters immediately (1.468209ms)
✔ should pass through multiple regular characters (0.129875ms)
✔ should handle unicode characters (0.115ms)
✔ Regular Characters (2.108083ms)
▶ Complete Escape Sequences
✔ should pass through complete mouse SGR sequences (0.240375ms)
✔ should pass through complete arrow key sequences (0.126458ms)
✔ should pass through complete function key sequences (0.108ms)
✔ should pass through meta key sequences (0.114084ms)
✔ should pass through SS3 sequences (0.081125ms)
✔ Complete Escape Sequences (0.806875ms)
▶ Partial Escape Sequences
✔ should buffer incomplete mouse SGR sequence (0.47575ms)
✔ should buffer incomplete CSI sequence (0.120625ms)
✔ should buffer split across many chunks (0.183542ms)
✔ should flush incomplete sequence after timeout (14.917875ms)
✔ Partial Escape Sequences (15.841584ms)
▶ Mixed Content
✔ should handle characters followed by escape sequence (0.10125ms)
✔ should handle escape sequence followed by characters (0.055ms)
✔ should handle multiple complete sequences (0.067ms)
✔ should handle partial sequence with preceding characters (0.070333ms)
✔ Mixed Content (0.354292ms)
▶ Kitty Keyboard Protocol
✔ should handle Kitty CSI u press events (0.08575ms)
✔ should handle Kitty CSI u release events (0.047ms)
✔ should handle batched Kitty press and release (0.05325ms)
✔ should handle multiple batched Kitty events (0.056959ms)
✔ should handle Kitty arrow keys with event type (0.042333ms)
✔ should handle Kitty functional keys with event type (0.043083ms)
✔ should handle plain characters mixed with Kitty sequences (0.041208ms)
✔ should handle Kitty sequence followed by plain characters (0.039917ms)
✔ should handle rapid typing simulation with Kitty protocol (0.063709ms)
✔ Kitty Keyboard Protocol (0.594ms)
▶ Mouse Events
✔ should handle mouse press event (0.075208ms)
✔ should handle mouse release event (0.042708ms)
✔ should handle mouse move event (0.055333ms)
✔ should handle split mouse events (0.059333ms)
✔ should handle multiple mouse events (0.049083ms)
✔ should handle old-style mouse sequence (ESC[M + 3 bytes) (0.04175ms)
✔ should buffer incomplete old-style mouse sequence (0.055917ms)
✔ Mouse Events (0.446917ms)
▶ Edge Cases
✔ should handle empty input (0.075583ms)
✔ should handle lone escape character with timeout (15.658708ms)
✔ should handle lone escape character with explicit flush (0.164833ms)
✔ should handle buffer input (0.066708ms)
✔ should handle very long sequences (0.210875ms)
✔ Edge Cases (16.278583ms)
▶ Flush
✔ should flush incomplete sequences (0.095708ms)
✔ should return empty array if nothing to flush (0.04675ms)
✔ should emit flushed data via timeout (15.597667ms)
✔ Flush (15.861917ms)
▶ Clear
✔ should clear buffered content without emitting (0.133375ms)
✔ Clear (0.171791ms)
▶ Bracketed Paste
✔ should emit paste event for complete bracketed paste (0.125958ms)
✔ should handle paste arriving in chunks (0.064334ms)
✔ should handle paste with input before and after (0.079834ms)
✔ should handle paste with newlines (0.060084ms)
✔ should handle paste with unicode (0.060834ms)
✔ Bracketed Paste (0.467959ms)
▶ Destroy
✔ should clear buffer on destroy (0.057208ms)
✔ should clear pending timeouts on destroy (15.813417ms)
✔ Destroy (15.949916ms)
✔ StdinBuffer (69.394042ms)
▶ TruncatedText component
✔ pads output lines to exactly match width (0.822041ms)
✔ pads output with vertical padding lines to width (0.17425ms)
✔ truncates long text and pads to width (1.081917ms)
✔ preserves ANSI codes in output and pads correctly (0.268917ms)
✔ truncates styled text and adds reset code before ellipsis (0.218417ms)
✔ handles text that fits exactly (0.074459ms)
✔ handles empty text (0.093167ms)
✔ stops at newline and only shows first line (0.067667ms)
✔ truncates first line even with newlines in text (0.146459ms)
✔ TruncatedText component (3.658583ms)
▶ TUI overlay compositing
✔ should not leak styles when a trailing reset sits beyond the last visible column (no overlay) (6.180167ms)
✔ should not leak styles when overlay slicing drops trailing SGR resets (4.634041ms)
✔ TUI overlay compositing (11.386ms)
▶ TUI differential rendering
✔ tracks cursor correctly when content shrinks with unchanged remaining lines (9.852333ms)
✔ renders correctly when only a middle line changes (spinner case) (6.60475ms)
✔ resets styles after each rendered line (2.105417ms)
✔ renders correctly when first line changes but rest stays same (3.256666ms)
✔ renders correctly when last line changes but rest stays same (3.556083ms)
✔ renders correctly when multiple non-adjacent lines change (3.290291ms)
✔ handles transition from content to empty and back to content (3.310416ms)
✔ TUI differential rendering (32.790375ms)
▶ wrapTextWithAnsi
▶ underline styling
✔ should not apply underline style before the styled text (2.0375ms)
✔ should not have whitespace before underline reset code (0.172916ms)
✔ should not bleed underline to padding - each line should end with reset for underline only (0.329ms)
✔ underline styling (2.914959ms)
▶ background color preservation
✔ should preserve background color across wrapped lines without full reset (0.191958ms)
✔ should reset underline but preserve background when wrapping underlined text inside background (0.272667ms)
✔ background color preservation (0.529625ms)
▶ basic wrapping
✔ should wrap plain text correctly (0.102667ms)
✔ should truncate trailing whitespace that exceeds width (0.069458ms)
✔ should preserve color codes across wraps (0.120958ms)
✔ basic wrapping (0.405459ms)
✔ wrapTextWithAnsi (4.155417ms)
ℹ tests 278
ℹ suites 63
ℹ pass 278
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 698.628333
Command exited with code 1The tests passed for tui package but coding-agent tests weren't found. Let me run them with the correct path:
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx vitest --run test/session-manager/file-operations.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/session-manager/file-operations.test.ts (17 tests) 34ms
Test Files 1 passed (1)
Tests 17 passed (17)
Start at 02:27:22
Duration 315ms (transform 48ms, setup 0ms, collect 52ms, tests 34ms, environment 0ms, prepare 47ms)All tests pass. Now let me add the changelog entry:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 429 files in 270ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 39ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.Implementation complete. Summary of changes:
packages/coding-agent/src/core/session-manager.ts:
setSessionFile(), when an existing file has no valid session entries (empty or corrupted), the file is now truncated and rewritten with a fresh session header immediately via _rewriteFile(), and flushed is set to true.packages/coding-agent/test/session-manager/file-operations.test.ts:
SessionManager.setSessionFile with corrupted files test suite with 4 tests:
truncates and rewrites empty file with valid headertruncates and rewrites file without valid headerpreserves explicit session file path when recovering from corrupted filesubsequent loads of recovered file work correctlypackages/coding-agent/CHANGELOG.md:
### Fixed crediting the original reporter.