Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 1334 characters to 1712 characters.

      ...
   78 This is the core abstraction shared by all modes. See full API design below.
   79
   80 ```typescript
   81 class AgentSession {
-  82   // State access
+  82   // ─── Read-only State Access ───
   83   get state(): AgentState;
   84   get model(): Model<any> | null;
   85   get thinkingLevel(): ThinkingLevel;
   86   get isStreaming(): boolean;
   87   get messages(): AppMessage[];  // Includes custom types like BashExecutionMessage
+  88   get queueMode(): QueueMode;
   88
-  89   // Event subscription (handles session persistence internally)
+  90   // ─── Event Subscription ───
+  91   // Handles session persistence internally (saves messages, checks auto-compaction)
   90   subscribe(listener: (event: AgentEvent) => void): () => void;
   91
-  92   // Prompting
+  94   // ─── Prompting ───
   93   prompt(text: string, options?: PromptOptions): Promise<void>;
   94   queueMessage(text: string): Promise<void>;
   95   clearQueue(): string[];
   96   abort(): Promise<void>;
   97   reset(): Promise<void>;
   98
-  99   // Model management
- 100   setModel(model: Model<any>): Promise<void>;
+ 101   // ─── Model Management ───
+ 102   setModel(model: Model<any>): Promise<void>;  // Validates API key, saves to session + settings
  101   cycleModel(): Promise<ModelCycleResult | null>;
  102   getAvailableModels(): Promise<Model<any>[]>;
  103
- 104   // Thinking level
- 105   setThinkingLevel(level: ThinkingLevel): void;
+ 106   // ─── Thinking Level ───
+ 107   setThinkingLevel(level: ThinkingLevel): void;  // Saves to session + settings
  106   cycleThinkingLevel(): ThinkingLevel | null;
  107   supportsThinking(): boolean;
  108
- 109   // Compaction
+ 111   // ─── Queue Mode ───
+ 112   setQueueMode(mode: QueueMode): void;  // Saves to settings
+ 113
+ 114   // ─── Compaction ───
  110   compact(customInstructions?: string): Promise<CompactionResult>;
  111   abortCompaction(): void;
- 112   checkAutoCompaction(): Promise<CompactionResult | null>;
- 113   setAutoCompactionEnabled(enabled: boolean): void;
+ 117   checkAutoCompaction(): Promise<CompactionResult | null>;  // Called internally after assistant messages
+ 118   setAutoCompactionEnabled(enabled: boolean): void;  // Saves to settings
  114   get autoCompactionEnabled(): boolean;
  115
- 116   // Bash execution
+ 121   // ─── Bash Execution ───
  117   executeBash(command: string, onChunk?: (chunk: string) => void): Promise<BashResult>;
  118   abortBash(): void;
  119   get isBashRunning(): boolean;
  120
      ...


Now let me add the queue mode implementation to the relevant work package. Let me find WP6 and add queue mode there or create a small addition:

bash

cd /Users/badlogic/workspaces/companion-mono && grep -n "WP6:" packages/coding-agent/docs/refactor.md


568:### WP6: AgentSession - Thinking Level Management


### WP6: AgentSession - Thinking Level Management
> Add setThinkingLevel(), cycleThinkingLevel(), supportsThinking() methods.

**Files to modify:**
- `src/core/agent-session.ts`

**Extract from:**
- `src/tui/tui-renderer.ts`: `cycleThinkingLevel()` method (lines ~940-970)

**Implementation:**
```typescript
// Add to AgentSession class

/**
 * Set thinking level. Silently uses "off" if model doesn't support it.
 * Saves to session and settings.
 */
setThinkingLevel(level: ThinkingLevel): void {
  const effectiveLevel = this.supportsThinking() ? level : "off";
  this.agent.setThinkingLevel(effectiveLevel);
  this.sessionManager.saveThinkingLevelChange(effectiveLevel);
  this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
}

/**
 * Cycle to next thinking level.
 * Returns new level, or null if model doesn't support thinking.
 */
cycleThinkingLevel(): ThinkingLevel | null {
  if (!this.supportsThinking()) return null;

  const modelId = this.model?.id || "";
  const supportsXhigh = modelId.includes("codex-max");
  const levels: ThinkingLevel[] = supportsXhigh
    ? ["off", "minimal", "low", "medium", "high", "xhigh"]
    : ["off", "minimal", "low", "medium", "high"];

  const currentIndex = levels.indexOf(this.thinkingLevel);
  const nextIndex = (currentIndex + 1) % levels.length;
  const nextLevel = levels[nextIndex];

  this.setThinkingLevel(nextLevel);
  return nextLevel;
}

/**
 * Check if current model supports thinking.
 */
supportsThinking(): boolean {
  return !!this.model?.reasoning;
}
```

**Verification:**
1. `npm run check` passes

- [ ] Add `setThinkingLevel()` method
- [ ] Add `cycleThinkingLevel()` method
- [ ] Add `supportsThinking()` method
- [ ] Verify with `npm run check`

---

### WP7: AgentSession - Compaction
> Add compact(), abortCompaction(), checkAutoCompaction(), autoCompactionEnabled methods.

**Files to modify:**
- `src/core/agent-session.ts`

**Extract from:**
- `src/tui/tui-renderer.ts`: `executeCompaction()` (lines ~2280-2370)
- `src/tui/tui-renderer.ts`: `checkAutoCompaction()` (lines ~495-525)
- `src/main.ts`: `runRpcMode()` auto-compaction logic (lines ~730-770)

**Implementation:**
```typescript
// Add to AgentSession class

export interface CompactionResult {
  tokensBefore: number;

[1027 more lines in file. Use offset=648 to continue]


edit

- [ ] Add `setThinkingLevel()` method
- [ ] Add `cycleThinkingLevel()` method
- [ ] Add `supportsThinking()` method
- [ ] Verify with `npm run check`

---

### WP7: AgentSession - Compaction

- [ ] Add `setThinkingLevel()` method
- [ ] Add `cycleThinkingLevel()` method
- [ ] Add `supportsThinking()` method
- [ ] Add `setQueueMode()` method and `queueMode` getter (see below)
- [ ] Verify with `npm run check`

**Queue mode (add to same WP):**
```typescript
// Add to AgentSession class

get queueMode(): QueueMode {
  return this.agent.getQueueMode();
}

/**
 * Set message queue mode. Saves to settings.
 */
setQueueMode(mode: QueueMode): void {
  this.agent.setQueueMode(mode);
  this.settingsManager.setQueueMode(mode);
}
```

---

### WP7: AgentSession - Compaction


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 190 characters to 579 characters.

      ...
  623
  624 - [ ] Add `setThinkingLevel()` method
  625 - [ ] Add `cycleThinkingLevel()` method
  626 - [ ] Add `supportsThinking()` method
+ 627 - [ ] Add `setQueueMode()` method and `queueMode` getter (see below)
  627 - [ ] Verify with `npm run check`
  628
+ 630 **Queue mode (add to same WP):**
+ 631 ```typescript
+ 632 // Add to AgentSession class
+ 633
+ 634 get queueMode(): QueueMode {
+ 635   return this.agent.getQueueMode();
+ 636 }
+ 637
+ 638 /**
+ 639  * Set message queue mode. Saves to settings.
+ 640  */
+ 641 setQueueMode(mode: QueueMode): void {
+ 642   this.agent.setQueueMode(mode);
+ 643   this.settingsManager.setQueueMode(mode);
+ 644 }
+ 645 ```
+ 646
  629 ---
  630
  631 ### WP7: AgentSession - Compaction
  632 > Add compact(), abortCompaction(), checkAutoCompaction(), autoCompactionEnabled methods.
      ...


bash

cd /Users/badlogic/workspaces/companion-mono && git add packages/coding-agent/docs/refactor.md && git commit -m "Add queue mode to AgentSession API, clarify setters vs getters"


Running formatting, linting, and type checking...

> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 229 files in 118ms. No fixes applied.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 44ms. No fixes applied.

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 17ms. No fixes applied.

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 25ms. No fixes applied.

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit

✅ All pre-commit checks passed!
[refactor cceb444] Add queue mode to AgentSession API, clarify setters vs getters
 1 file changed, 34 insertions(+), 11 deletions(-)


Updated. The API now clearly shows:

**Read-only getters:**
- `state`, `model`, `thinkingLevel`, `isStreaming`, `messages`, `queueMode`, `autoCompactionEnabled`, `isBashRunning`

**Setters (as methods, with side effects):**
- `setModel()` - validates API key, saves to session + settings
- `setThinkingLevel()` - saves to session + settings
- `setQueueMode()` - saves to settings
- `setAutoCompactionEnabled()` - saves to settings

**Cycle methods (get + set combined):**
- `cycleModel()` - cycles and persists, returns result
- `cycleThinkingLevel()` - cycles and persists, returns new level

The pattern is: getters are cheap reads, setters are methods because they have side effects (persistence, validation).

Ready to start WP1?


ok, this looks amazing, start implementing


The user wants me to start implementing the refactoring plan. Let me start with WP1: Create bash-executor.ts.

I need to:
1. Create `src/core/bash-executor.ts`
2. Extract the bash execution logic from tui-renderer.ts and main.ts
3. Make it a standalone module with proper types

Let me first read the relevant sections from both files to understand the full implementation.

Let's start with WP1: Create bash-executor.ts.

First, let me read the existing implementations to extract:

cd /Users/badlogic/workspaces/companion-mono/packages/coding-agent && grep -n "executeBashCommand\|executeRpcBashCommand" src/main.ts src/tui/tui-renderer.ts


src/main.ts:867: * Similar to tui-renderer's executeBashCommand but without streaming callbacks.
src/main.ts:869:async function executeRpcBashCommand(command: string): Promise<{
src/main.ts:1079:					const result = await executeRpcBashCommand(input.command);
src/tui/tui-renderer.ts:2082:			const result = await this.executeBashCommand(command, (chunk) => {
src/tui/tui-renderer.ts:2127:	private executeBashCommand(


				if (tempFileStream) {
					tempFileStream.write(text);
				}

				// Keep rolling buffer of sanitized text
				outputChunks.push(text);
				outputBytes += text.length;
				while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
					const removed = outputChunks.shift()!;
					outputBytes -= removed.length;
				}

				// Stream to component
				onChunk(text);
			};

			child.stdout?.on("data", handleData);
			child.stderr?.on("data", handleData);

			child.on("close", (code) => {
				if (tempFileStream) {
					tempFileStream.end();
				}

				this.bashProcess = null;

				// Combine buffered chunks for truncation (already sanitized)
				const fullOutput = outputChunks.join("");
				const truncationResult = truncateTail(fullOutput);

				// code === null means killed (cancelled)
				const cancelled = code === null;

				resolve({
					exitCode: code,
					cancelled,
					truncationResult: truncationResult.truncated ? truncationResult : undefined,
					fullOutputPath: tempFilePath,
				});
			});

			child.on("error", (err) => {
				if (tempFileStream) {
					tempFileStream.end();
				}
				this.bashProcess = null;
				reject(err);
			});
		});
	}

	private compactionAbortController: AbortController | null = null;

	/**
	 * Shared logic to execute context compaction.
	 * Handles aborting agent, showing loader, performing compaction, updating session/UI.
	 */
	private async executeCompaction(customInstructions?: string, isAuto = false): Promise<void> {
		// Unsubscribe first to prevent processing events during compaction
		this.unsubscribe?.();

		// Abort and wait for completion
		this.agent.abort();
		await this.agent.waitForIdle();

		// Stop loading animation
		if (this.loadingAnimation) {
			this.loadingAnimation.stop();
			this.loadingAnimation = null;
		}
		this.statusContainer.clear();

		// Create abort controller for compaction
		this.compactionAbortController = new AbortController();

		// Set up escape handler during compaction
		const originalOnEscape = this.editor.onEscape;
		this.editor.onEscape = () => {
			if (this.compactionAbortController) {
				this.compactionAbortController.abort();
			}
		};

		// Show compacting status with loader
		this.chatContainer.addChild(new Spacer(1));
		const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
		const compactingLoader = new Loader(
			this.ui,
			(spinner) => theme.fg("accent", spinner),
			(text) => theme.fg("muted", text),
			label,
		);
		this.statusContainer.addChild(compactingLoader);
		this.ui.requestRender();

		try {
			// Get API key for current model
			const apiKey = await getApiKeyForModel(this.agent.state.model);
			if (!apiKey) {
				throw new Error(`No API key for ${this.agent.state.model.provider}`);
			}

			// Perform compaction with abort signal
			const entries = this.sessionManager.loadEntries();
			const settings = this.settingsManager.getCompactionSettings();
			const compactionEntry = await compact(
				entries,
				this.agent.state.model,
				settings,
				apiKey,
				this.compactionAbortController.signal,
				customInstructions,
			);

			// Check if aborted after compact returned
			if (this.compactionAbortController.signal.aborted) {
				throw new Error("Compaction cancelled");
			}

[88 more lines in file. Use offset=2290 to continue]


				}
				continue;
			}
			// No exact provider/model match - fall through to other matching
		}

		// Check for exact ID match (case-insensitive)
		const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
		if (exactMatch) {
			// Exact match found - use it directly
			if (!scopedModels.find((sm) => sm.model.id === exactMatch.id && sm.model.provider === exactMatch.provider)) {
				scopedModels.push({ model: exactMatch, thinkingLevel });
			}
			continue;
		}

		// No exact match - fall back to partial matching
		const matches = availableModels.filter(
			(m) =>
				m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
				m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
		);

		if (matches.length === 0) {
			console.warn(chalk.yellow(`Warning: No models match pattern "${modelPattern}"`));
			continue;
		}

		// Helper to check if a model ID looks like an alias (no date suffix)
		// Dates are typically in format: -20241022 or -20250929
		const isAlias = (id: string): boolean => {
			// Check if ID ends with -latest
			if (id.endsWith("-latest")) return true;

			// Check if ID ends with a date pattern (-YYYYMMDD)
			const datePattern = /-\d{8}$/;
			return !datePattern.test(id);
		};

		// Separate into aliases and dated versions
		const aliases = matches.filter((m) => isAlias(m.id));
		const datedVersions = matches.filter((m) => !isAlias(m.id));

		let bestMatch: Model<Api>;

		if (aliases.length > 0) {
			// Prefer alias - if multiple aliases, pick the one that sorts highest
			aliases.sort((a, b) => b.id.localeCompare(a.id));
			bestMatch = aliases[0];
		} else {
			// No alias found, pick latest dated version
			datedVersions.sort((a, b) => b.id.localeCompare(a.id));
			bestMatch = datedVersions[0];
		}

		// Avoid duplicates
		if (!scopedModels.find((sm) => sm.model.id === bestMatch.id && sm.model.provider === bestMatch.provider)) {
			scopedModels.push({ model: bestMatch, thinkingLevel });
		}
	}

	return scopedModels;
}

async function selectSession(sessionManager: SessionManager): Promise<string | null> {
	return new Promise((resolve) => {
		const ui = new TUI(new ProcessTerminal());
		let resolved = false;

		const selector = new SessionSelectorComponent(

[835 more lines in file. Use offset=710 to continue]


Let me get the right section of main.ts for executeRpcBashCommand:


			}
		}
	}
}

/**
 * Execute a bash command for RPC mode.
 * Similar to tui-renderer's executeBashCommand but without streaming callbacks.
 */
async function executeRpcBashCommand(command: string): Promise<{
	output: string;
	exitCode: number | null;
	truncationResult?: ReturnType<typeof truncateTail>;
	fullOutputPath?: string;
}> {
	return new Promise((resolve, reject) => {
		const { shell, args } = getShellConfig();
		const child = spawn(shell, [...args, command], {
			detached: true,
			stdio: ["ignore", "pipe", "pipe"],
		});

		const chunks: Buffer[] = [];
		let chunksBytes = 0;
		const maxChunksBytes = DEFAULT_MAX_BYTES * 2;

		let tempFilePath: string | undefined;
		let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
		let totalBytes = 0;

		const handleData = (data: Buffer) => {
			totalBytes += data.length;

			// Start writing to temp file if exceeds threshold
			if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
				const id = randomBytes(8).toString("hex");
				tempFilePath = join(tmpdir(), `companion-bash-${id}.log`);
				tempFileStream = createWriteStream(tempFilePath);
				for (const chunk of chunks) {
					tempFileStream.write(chunk);
				}
			}

			if (tempFileStream) {
				tempFileStream.write(data);
			}

			// Keep rolling buffer
			chunks.push(data);
			chunksBytes += data.length;
			while (chunksBytes > maxChunksBytes && chunks.length > 1) {
				const removed = chunks.shift()!;
				chunksBytes -= removed.length;
			}
		};

		child.stdout?.on("data", handleData);
		child.stderr?.on("data", handleData);

		child.on("close", (code) => {
			if (tempFileStream) {
				tempFileStream.end();
			}

			// Combine buffered chunks
			const fullBuffer = Buffer.concat(chunks);
			const fullOutput = stripAnsi(fullBuffer.toString("utf-8")).replace(/\r/g, "");
			const truncationResult = truncateTail(fullOutput);

			resolve({
				output: fullOutput,
				exitCode: code,
				truncationResult: truncationResult.truncated ? truncationResult : undefined,
				fullOutputPath: tempFilePath,
			});
		});

		child.on("error", (err) => {
			if (tempFileStream) {
				tempFileStream.end();

[605 more lines in file. Use offset=940 to continue]


			this.showError(`Bash command failed: ${errorMessage}`);
		}

		this.bashComponent = null;
		this.ui.requestRender();
	}

	private executeBashCommand(
		command: string,
		onChunk: (chunk: string) => void,
	): Promise<{
		exitCode: number | null;
		cancelled: boolean;
		truncationResult?: TruncationResult;
		fullOutputPath?: string;
	}> {
		return new Promise((resolve, reject) => {
			const { shell, args } = getShellConfig();
			const child = spawn(shell, [...args, command], {
				detached: true,
				stdio: ["ignore", "pipe", "pipe"],
			});

			this.bashProcess = child;

			// Track sanitized output for truncation
			const outputChunks: string[] = [];
			let outputBytes = 0;
			const maxOutputBytes = DEFAULT_MAX_BYTES * 2;

			// Temp file for large output
			let tempFilePath: string | undefined;
			let tempFileStream: WriteStream | undefined;
			let totalBytes = 0;

			const handleData = (data: Buffer) => {
				totalBytes += data.length;

				// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
				const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");

				// Start writing to temp file if exceeds threshold
				if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
					const id = randomBytes(8).toString("hex");
					tempFilePath = join(tmpdir(), `companion-bash-${id}.log`);
					tempFileStream = createWriteStream(tempFilePath);
					for (const chunk of outputChunks) {
						tempFileStream.write(chunk);
					}
				}

				if (tempFileStream) {
					tempFileStream.write(text);
				}

				// Keep rolling buffer of sanitized text
				outputChunks.push(text);
				outputBytes += text.length;
				while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
					const removed = outputChunks.shift()!;
					outputBytes -= removed.length;
				}

				// Stream to component
				onChunk(text);
			};

			child.stdout?.on("data", handleData);
			child.stderr?.on("data", handleData);

[188 more lines in file. Use offset=2190 to continue]


Now I have both implementations. Let me create the unified bash-executor.ts:


(no output)


write

/**
 * Bash command execution with streaming support and cancellation.
 *
 * This module provides a unified bash execution implementation used by:
 * - AgentSession.executeBash() for interactive and RPC modes
 * - Direct calls from modes that need bash execution
 */

import { randomBytes } from "node:crypto";
import { createWriteStream, type WriteStream } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawn, type ChildProcess } from "child_process";
import stripAnsi from "strip-ansi";
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../shell.js";
import { DEFAULT_MAX_BYTES, truncateTail } from "../tools/truncate.js";

// ============================================================================
// Types
// ============================================================================

export interface BashExecutorOptions {
	/** Callback for streaming output chunks (already sanitized) */
	onChunk?: (chunk: string) => void;
	/** AbortSignal for cancellation */
	signal?: AbortSignal;
}

export interface BashResult {
	/** Combined stdout + stderr output (sanitized, possibly truncated) */
	output: string;
	/** Process exit code (null if killed/cancelled) */
	exitCode: number | null;
	/** Whether the command was cancelled via signal */
	cancelled: boolean;
	/** Whether the output was truncated */
	truncated: boolean;
	/** Path to temp file containing full output (if output exceeded threshold) */
	fullOutputPath?: string;
}

// ============================================================================
// Implementation
// ============================================================================

/**
 * Execute a bash command with optional streaming and cancellation support.
 *
 * Features:
 * - Streams sanitized output via onChunk callback
 * - Writes large output to temp file for later retrieval
 * - Supports cancellation via AbortSignal
 * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)
 * - Truncates output if it exceeds the default max bytes
 *
 * @param command - The bash command to execute
 * @param options - Optional streaming callback and abort signal
 * @returns Promise resolving to execution result
 */
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
	return new Promise((resolve, reject) => {
		const { shell, args } = getShellConfig();
		const child: ChildProcess = spawn(shell, [...args, command], {
			detached: true,
			stdio: ["ignore", "pipe", "pipe"],
		});

		// Track sanitized output for truncation
		const outputChunks: string[] = [];
		let outputBytes = 0;
		const maxOutputBytes = DEFAULT_MAX_BYTES * 2;

		// Temp file for large output
		let tempFilePath: string | undefined;
		let tempFileStream: WriteStream | undefined;
		let totalBytes = 0;

		// Handle abort signal
		const abortHandler = () => {
			if (child.pid) {
				killProcessTree(child.pid);
			}
		};

		if (options?.signal) {
			if (options.signal.aborted) {
				// Already aborted, don't even start
				child.kill();
				resolve({
					output: "",
					exitCode: null,
					cancelled: true,
					truncated: false,
				});
				return;
			}
			options.signal.addEventListener("abort", abortHandler, { once: true });
		}

		const handleData = (data: Buffer) => {
			totalBytes += data.length;

			// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
			const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");

			// Start writing to temp file if exceeds threshold
			if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
				const id = randomBytes(8).toString("hex");
				tempFilePath = join(tmpdir(), `companion-bash-${id}.log`);
				tempFileStream = createWriteStream(tempFilePath);
				// Write already-buffered chunks to temp file
				for (const chunk of outputChunks) {
					tempFileStream.write(chunk);
				}
			}

			if (tempFileStream) {
				tempFileStream.write(text);
			}

			// Keep rolling buffer of sanitized text
			outputChunks.push(text);
			outputBytes += text.length;
			while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
				const removed = outputChunks.shift()!;
				outputBytes -= removed.length;
			}

			// Stream to callback if provided
			if (options?.onChunk) {
				options.onChunk(text);
			}
		};

		child.stdout?.on("data", handleData);
		child.stderr?.on("data", handleData);

		child.on("close", (code) => {
			// Clean up abort listener
			if (options?.signal) {
				options.signal.removeEventListener("abort", abortHandler);
			}

			if (tempFileStream) {
				tempFileStream.end();
			}

			// Combine buffered chunks for truncation (already sanitized)
			const fullOutput = outputChunks.join("");
			const truncationResult = truncateTail(fullOutput);

			// code === null means killed (cancelled)
			const cancelled = code === null;

			resolve({
				output: truncationResult.truncated ? truncationResult.content : fullOutput,
				exitCode: code,
				cancelled,
				truncated: truncationResult.truncated,
				fullOutputPath: tempFilePath,
			});
		});

		child.on("error", (err) => {
			// Clean up abort listener
			if (options?.signal) {
				options.signal.removeEventListener("abort", abortHandler);
			}

			if (tempFileStream) {
				tempFileStream.end();
			}

			reject(err);
		});
	});
}


Successfully wrote 5370 bytes to /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/bash-executor.ts


Now create the barrel export:

/**
 * Core modules shared between all run modes.
 */

export { executeBash, type BashExecutorOptions, type BashResult } from "./bash-executor.js";


Successfully wrote 148 bytes to /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/index.ts


bash

cd /Users/badlogic/workspaces/companion-mono && npm run check


> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 231 files in 130ms. Fixed 2 files.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 42ms. No fixes applied.

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 14ms. No fixes applied.

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 18ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 23ms. No fixes applied.

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit


Now let me update the checkboxes in the refactor plan and commit:

edit

- [ ] Create `src/core/bash-executor.ts` with `executeBash()` function
- [ ] Add proper TypeScript types and exports
- [ ] Verify with `npm run check`

- [x] Create `src/core/bash-executor.ts` with `executeBash()` function
- [x] Add proper TypeScript types and exports
- [x] Verify with `npm run check`


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 150 characters to 150 characters.

      ...
  181 1. `npm run check` passes
  182 2. Manual test: Run `companion` in interactive mode, execute `!ls -la`, verify output appears
  183 3. Manual test: Run `!sleep 10`, press Esc, verify cancellation works
  184
- 185 - [ ] Create `src/core/bash-executor.ts` with `executeBash()` function
- 186 - [ ] Add proper TypeScript types and exports
- 187 - [ ] Verify with `npm run check`
+ 185 - [x] Create `src/core/bash-executor.ts` with `executeBash()` function
+ 186 - [x] Add proper TypeScript types and exports
+ 187 - [x] Verify with `npm run check`
  188
  189 ---
  190
  191 ### WP2: Create agent-session.ts (Core Structure)
      ...


bash

cd /Users/badlogic/workspaces/companion-mono && git add -A && git commit -m "WP1: Create bash-executor.ts with unified bash execution"


Running formatting, linting, and type checking...

> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 231 files in 153ms. No fixes applied.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 44ms. No fixes applied.

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 14ms. No fixes applied.

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 18ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 4ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 25ms. No fixes applied.

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit

✅ All pre-commit checks passed!
[refactor 3f30550] WP1: Create bash-executor.ts with unified bash execution
 3 files changed, 185 insertions(+), 3 deletions(-)
 create mode 100644 packages/coding-agent/src/core/bash-executor.ts
 create mode 100644 packages/coding-agent/src/core/index.ts


WP1 complete. Now WP2: Create AgentSession basic structure.

write

/**
 * AgentSession - Core abstraction for agent lifecycle and session management.
 *
 * This class is shared between all run modes (interactive, print, rpc).
 * It encapsulates:
 * - Agent state access
 * - Event subscription with automatic session persistence
 * - Model and thinking level management
 * - Compaction (manual and auto)
 * - Bash execution
 * - Session switching and branching
 *
 * Modes use this class and add their own I/O layer on top.
 */

import type { Agent, AgentEvent, AgentState, AppMessage, ThinkingLevel, QueueMode } from "@mariozechner/companion-agent-core";
import type { AssistantMessage, Model } from "@mariozechner/companion-ai";
import type { SessionManager } from "../session-manager.js";
import type { SettingsManager } from "../settings-manager.js";
import type { FileSlashCommand } from "../slash-commands.js";

// ============================================================================
// Types
// ============================================================================

export interface AgentSessionConfig {
	agent: Agent;
	sessionManager: SessionManager;
	settingsManager: SettingsManager;
	/** Models to cycle through with Ctrl+P (from --models flag) */
	scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	/** File-based slash commands for expansion */
	fileCommands?: FileSlashCommand[];
}

// ============================================================================
// AgentSession Class
// ============================================================================

export class AgentSession {
	readonly agent: Agent;
	readonly sessionManager: SessionManager;
	readonly settingsManager: SettingsManager;

	private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	private _fileCommands: FileSlashCommand[];

	constructor(config: AgentSessionConfig) {
		this.agent = config.agent;
		this.sessionManager = config.sessionManager;
		this.settingsManager = config.settingsManager;
		this._scopedModels = config.scopedModels ?? [];
		this._fileCommands = config.fileCommands ?? [];
	}

	// =========================================================================
	// Read-only State Access
	// =========================================================================

	/** Full agent state */
	get state(): AgentState {
		return this.agent.state;
	}

	/** Current model (may be null if not yet selected) */
	get model(): Model<any> | null {
		return this.agent.state.model;
	}

	/** Current thinking level */
	get thinkingLevel(): ThinkingLevel {
		return this.agent.state.thinkingLevel;
	}

	/** Whether agent is currently streaming a response */
	get isStreaming(): boolean {
		return this.agent.state.isStreaming;
	}

	/** All messages including custom types like BashExecutionMessage */
	get messages(): AppMessage[] {
		return this.agent.state.messages;
	}

	/** Current queue mode */
	get queueMode(): QueueMode {
		return this.agent.getQueueMode();
	}

	/** Current session file path */
	get sessionFile(): string {
		return this.sessionManager.getSessionFile();
	}

	/** Current session ID */
	get sessionId(): string {
		return this.sessionManager.getSessionId();
	}

	/** Scoped models for cycling (from --models flag) */
	get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel: ThinkingLevel }> {
		return this._scopedModels;
	}

	/** File-based slash commands */
	get fileCommands(): ReadonlyArray<FileSlashCommand> {
		return this._fileCommands;
	}
}


Successfully wrote 3473 bytes to /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/agent-session.ts


edit

/**
 * Core modules shared between all run modes.
 */

export { executeBash, type BashExecutorOptions, type BashResult } from "./bash-executor.js";
export { AgentSession, type AgentSessionConfig } from "./agent-session.js";


Could not find the exact text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/index.ts. The old text must match exactly including all whitespace and newlines.


/**
 * Core modules shared between all run modes.
 */

export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";


/**
 * Core modules shared between all run modes.
 */

export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
export { AgentSession, type AgentSessionConfig } from "./agent-session.js";


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/index.ts. Changed 147 characters to 223 characters.

   ...
 2  * Core modules shared between all run modes.
 3  */
 4
 5 export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
+6 export { AgentSession, type AgentSessionConfig } from "./agent-session.js";


> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

packages/coding-agent/src/core/agent-session.ts:16:22 lint/correctness/noUnusedImports  FIXABLE  ━━━━━━━━━━

  ! Several of these imports are unused.

    14 │  */
    15 │
  > 16 │ import type { Agent, AgentEvent, AgentState, AppMessage, QueueMode, ThinkingLevel } from "@mariozechner/companion-agent-core";
       │                      ^^^^^^^^^^
    17 │ import type { AssistantMessage, Model } from "@mariozechner/companion-ai";
    18 │ import type { SessionManager } from "../session-manager.js";

  i Unused imports might be the result of an incomplete refactoring.

  i Unsafe fix: Remove the unused imports.

    16 │ import·type·{·Agent,·AgentEvent,·AgentState,·AppMessage,·QueueMode,·ThinkingLevel·}·from·"@mariozechner/companion-agent-core";
       │                      ------------

packages/coding-agent/src/core/agent-session.ts:17:15 lint/correctness/noUnusedImports  FIXABLE  ━━━━━━━━━━

  ! Several of these imports are unused.

    16 │ import type { Agent, AgentEvent, AgentState, AppMessage, QueueMode, ThinkingLevel } from "@mariozechner/companion-agent-core";
  > 17 │ import type { AssistantMessage, Model } from "@mariozechner/companion-ai";
       │               ^^^^^^^^^^^^^^^^
    18 │ import type { SessionManager } from "../session-manager.js";
    19 │ import type { SettingsManager } from "../settings-manager.js";

  i Unused imports might be the result of an incomplete refactoring.

  i Unsafe fix: Remove the unused imports.

    17 │ import·type·{·AssistantMessage,·Model·}·from·"@mariozechner/companion-ai";
       │               ------------------

Skipped 2 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe

Checked 232 files in 123ms. Fixed 2 files.
Found 2 warnings.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

../coding-agent/src/core/agent-session.ts(16,58): error TS2305: Module '"@mariozechner/companion-agent-core"' has no exported member 'QueueMode'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/companion-mono/packages/agent
npm error workspace @mariozechner/companion-agent-core@0.14.2
npm error location /Users/badlogic/workspaces/companion-mono/packages/agent
npm error command failed
npm error command sh -c tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 56ms. No fixes applied.
../coding-agent/src/core/agent-session.ts(16,58): error TS2305: Module '"@mariozechner/companion-agent-core"' has no exported member 'QueueMode'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/companion-mono/packages/ai
npm error workspace @mariozechner/companion-ai@0.14.2
npm error location /Users/badlogic/workspaces/companion-mono/packages/ai
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

src/core/agent-session.ts(16,58): error TS2305: Module '"@mariozechner/companion-agent-core"' has no exported member 'QueueMode'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/companion-mono/packages/coding-agent
npm error workspace @mariozechner/companion-coding-agent@0.14.2
npm error location /Users/badlogic/workspaces/companion-mono/packages/coding-agent
npm error command failed
npm error command sh -c tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 16ms. No fixes applied.
../coding-agent/src/core/agent-session.ts(16,58): error TS2305: Module '"@mariozechner/companion-agent-core"' has no exported member 'QueueMode'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/companion-mono/packages/mom
npm error workspace @mariozechner/companion-mom@0.14.2
npm error location /Users/badlogic/workspaces/companion-mono/packages/mom
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 4ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 23ms. No fixes applied.
../coding-agent/src/core/agent-session.ts(16,58): error TS2305: Module '"@mariozechner/companion-agent-core"' has no exported member 'QueueMode'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/companion-mono/packages/tui
npm error workspace @mariozechner/companion-tui@0.14.2
npm error location /Users/badlogic/workspaces/companion-mono/packages/tui
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit

Command exited with code 2


Need to check the correct type for QueueMode:

bash

cd /Users/badlogic/workspaces/companion-mono && grep -r "QueueMode\|queueMode\|getQueueMode" packages/agent/src/ --include="*.ts" | head -20


packages/agent/src/agent.ts:	queueMode?: "all" | "one-at-a-time";
packages/agent/src/agent.ts:	private queueMode: "all" | "one-at-a-time";
packages/agent/src/agent.ts:		this.queueMode = opts.queueMode || "one-at-a-time";
packages/agent/src/agent.ts:	setQueueMode(mode: "all" | "one-at-a-time") {
packages/agent/src/agent.ts:		this.queueMode = mode;
packages/agent/src/agent.ts:	getQueueMode(): "all" | "one-at-a-time" {
packages/agent/src/agent.ts:		return this.queueMode;
packages/agent/src/agent.ts:				if (this.queueMode === "one-at-a-time") {


It's not exported as a type. Let me fix by using the inline type:

edit

import type { Agent, AgentEvent, AgentState, AppMessage, ThinkingLevel, QueueMode } from "@mariozechner/companion-agent-core";
import type { AssistantMessage, Model } from "@mariozechner/companion-ai";

import type { Agent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { Model } from "@mariozechner/companion-ai";


Could not find the exact text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/agent-session.ts. The old text must match exactly including all whitespace and newlines.


btw, can we make it so we keep the old code around for reference? i think your plan already works like this, except for main.ts. can have a new main.ts, just call it main-new and cli-new if needed, then i can compare in parallel as i clean up whatever you come up with.


The user wants to keep the old code around for reference while we build the new implementation. They suggest:
1. Creating new files like `main-new.ts` and `cli-new.ts` instead of modifying the existing ones
2. This allows parallel comparison during cleanup

This is a good idea - it means we can:
1. Build the new implementation without breaking the existing one
2. Test the new implementation alongside the old one
3. Compare behavior and do a final switchover when ready

Let me first fix the current TypeScript error, then adjust the plan to use this approach.

Good idea. Let me first fix the current error, then update the plan to use that approach.


/**
 * AgentSession - Core abstraction for agent lifecycle and session management.
 *
 * This class is shared between all run modes (interactive, print, rpc).
 * It encapsulates:
 * - Agent state access
 * - Event subscription with automatic session persistence
 * - Model and thinking level management
 * - Compaction (manual and auto)
 * - Bash execution
 * - Session switching and branching
 *
 * Modes use this class and add their own I/O layer on top.
 */

import type { Agent, AgentEvent, AgentState, AppMessage, QueueMode, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { AssistantMessage, Model } from "@mariozechner/companion-ai";
import type { SessionManager } from "../session-manager.js";
import type { SettingsManager } from "../settings-manager.js";
import type { FileSlashCommand } from "../slash-commands.js";

// ============================================================================
// Types
// ============================================================================

export interface AgentSessionConfig {
	agent: Agent;
	sessionManager: SessionManager;
	settingsManager: SettingsManager;
	/** Models to cycle through with Ctrl+P (from --models flag) */
	scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	/** File-based slash commands for expansion */
	fileCommands?: FileSlashCommand[];
}

// ============================================================================
// AgentSession Class
// ============================================================================

export class AgentSession {
	readonly agent: Agent;
	readonly sessionManager: SessionManager;
	readonly settingsManager: SettingsManager;

	private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	private _fileCommands: FileSlashCommand[];

	constructor(config: AgentSessionConfig) {
		this.agent = config.agent;
		this.sessionManager = config.sessionManager;
		this.settingsManager = config.settingsManager;
		this._scopedModels = config.scopedModels ?? [];
		this._fileCommands = config.fileCommands ?? [];
	}

	// =========================================================================
	// Read-only State Access
	// =========================================================================

	/** Full agent state */
	get state(): AgentState {
		return this.agent.state;
	}

	/** Current model (may be null if not yet selected) */
	get model(): Model<any> | null {
		return this.agent.state.model;
	}

	/** Current thinking level */
	get thinkingLevel(): ThinkingLevel {
		return this.agent.state.thinkingLevel;
	}

	/** Whether agent is currently streaming a response */
	get isStreaming(): boolean {
		return this.agent.state.isStreaming;
	}

	/** All messages including custom types like BashExecutionMessage */
	get messages(): AppMessage[] {
		return this.agent.state.messages;
	}

	/** Current queue mode */
	get queueMode(): QueueMode {
		return this.agent.getQueueMode();
	}

	/** Current session file path */
	get sessionFile(): string {
		return this.sessionManager.getSessionFile();
	}

	/** Current session ID */
	get sessionId(): string {
		return this.sessionManager.getSessionId();
	}

	/** Scoped models for cycling (from --models flag) */
	get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel: ThinkingLevel }> {
		return this._scopedModels;
	}

	/** File-based slash commands */
	get fileCommands(): ReadonlyArray<FileSlashCommand> {
		return this._fileCommands;
	}
}


import type { Agent, AgentEvent, AgentState, AppMessage, QueueMode, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { AssistantMessage, Model } from "@mariozechner/companion-ai";


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/agent-session.ts. Changed 187 characters to 146 characters.

     ...
  12  *
  13  * Modes use this class and add their own I/O layer on top.
  14  */
  15
- 16 import type { Agent, AgentEvent, AgentState, AppMessage, QueueMode, ThinkingLevel } from "@mariozechner/companion-agent-core";
- 17 import type { AssistantMessage, Model } from "@mariozechner/companion-ai";
+ 16 import type { Agent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
+ 17 import type { Model } from "@mariozechner/companion-ai";
  18 import type { SessionManager } from "../session-manager.js";
  19 import type { SettingsManager } from "../settings-manager.js";
  20 import type { FileSlashCommand } from "../slash-commands.js";
  21
     ...


	/** Current queue mode */
	get queueMode(): QueueMode {
		return this.agent.getQueueMode();
	}

	/** Current queue mode */
	get queueMode(): "all" | "one-at-a-time" {
		return this.agent.getQueueMode();
	}


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/agent-session.ts. Changed 95 characters to 109 characters.

     ...
  82 		return this.agent.state.messages;
  83 	}
  84
  85 	/** Current queue mode */
- 86 	get queueMode(): QueueMode {
+ 86 	get queueMode(): "all" | "one-at-a-time" {
  87 		return this.agent.getQueueMode();
  88 	}
  89
  90 	/** Current session file path */
     ...


> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 232 files in 121ms. No fixes applied.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 46ms. No fixes applied.

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 14ms. No fixes applied.

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 4ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 23ms. No fixes applied.

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit


Now let me update the refactor plan to use the "keep old code" approach:


# Coding Agent Refactoring Plan

## Status

**Branch:** `refactor`
**Started:** 2024-12-08

To resume work on this refactoring:
1. Read this document fully
2. Run `git diff` to see current work in progress
3. Check the work packages below - find first unchecked item
4. Read any files mentioned in that work package before making changes

---

## Goals

1. **Eliminate code duplication** between the three run modes (interactive, print/json, rpc)
2. **Create a testable core** (`AgentSession`) that encapsulates all agent/session logic
3. **Separate concerns**: TUI rendering vs agent state management vs I/O
4. **Improve naming**: `TuiRenderer` → `InteractiveMode` (it's not just a renderer)
5. **Simplify main.ts**: Move setup logic out, make it just arg parsing + mode routing

---

## Architecture Overview

### Current State (Problems)

```
main.ts (1100+ lines)
├── parseArgs, printHelp
├── buildSystemPrompt, loadProjectContextFiles
├── resolveModelScope, model resolution logic
├── runInteractiveMode() - thin wrapper around TuiRenderer
├── runSingleShotMode() - duplicates event handling, session saving
├── runRpcMode() - duplicates event handling, session saving, auto-compaction, bash execution
└── executeRpcBashCommand() - duplicate of TuiRenderer.executeBashCommand()

tui/tui-renderer.ts (2400+ lines)
├── TUI lifecycle (init, render, event loop)
├── Agent event handling + session persistence (duplicated in main.ts)
├── Auto-compaction logic (duplicated in main.ts runRpcMode)
├── Bash execution (duplicated in main.ts)
├── All slash command implementations (/export, /copy, /model, /thinking, etc.)
├── All hotkey handlers (Ctrl+C, Ctrl+P, Shift+Tab, etc.)
├── Model/thinking cycling logic
└── 6 different selector UIs (model, thinking, theme, session, branch, oauth)
```

[1642 more lines in file. Use offset=51 to continue]


## Status

**Branch:** `refactor`
**Started:** 2024-12-08

To resume work on this refactoring:
1. Read this document fully
2. Run `git diff` to see current work in progress
3. Check the work packages below - find first unchecked item
4. Read any files mentioned in that work package before making changes

## Strategy: Keep Old Code for Reference

We create new files alongside old ones instead of modifying in place:
- `src/modes/print-mode.ts` (new) - old code stays in `main.ts`
- `src/modes/rpc-mode.ts` (new) - old code stays in `main.ts`
- `src/modes/interactive/interactive-mode.ts` (new) - old code stays in `tui/tui-renderer.ts`
- `src/main-new.ts` (new) - old code stays in `main.ts`
- `src/cli-new.ts` (new) - old code stays in `cli.ts`

This allows:
- Parallel comparison of old vs new behavior
- Gradual migration and testing
- Easy rollback if needed

Final switchover: When everything works, rename files and delete old code.


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 304 characters to 940 characters.

      ...
   10 2. Run `git diff` to see current work in progress
   11 3. Check the work packages below - find first unchecked item
   12 4. Read any files mentioned in that work package before making changes
   13
+  14 ## Strategy: Keep Old Code for Reference
+  15
+  16 We create new files alongside old ones instead of modifying in place:
+  17 - `src/modes/print-mode.ts` (new) - old code stays in `main.ts`
+  18 - `src/modes/rpc-mode.ts` (new) - old code stays in `main.ts`
+  19 - `src/modes/interactive/interactive-mode.ts` (new) - old code stays in `tui/tui-renderer.ts`
+  20 - `src/main-new.ts` (new) - old code stays in `main.ts`
+  21 - `src/cli-new.ts` (new) - old code stays in `cli.ts`
+  22
+  23 This allows:
+  24 - Parallel comparison of old vs new behavior
+  25 - Gradual migration and testing
+  26 - Easy rollback if needed
+  27
+  28 Final switchover: When everything works, rename files and delete old code.
+  29
   14 ---
   15
   16 ## Goals
   17
      ...


Now let me update the work packages to reflect this. First let me check the WP for main.ts:

bash

cd /Users/badlogic/workspaces/companion-mono && grep -n "WP14\|WP11\|WP12\|main.ts" packages/coding-agent/docs/refactor.md | head -30


17:- `src/modes/print-mode.ts` (new) - old code stays in `main.ts`
18:- `src/modes/rpc-mode.ts` (new) - old code stays in `main.ts`
20:- `src/main-new.ts` (new) - old code stays in `main.ts`
38:5. **Simplify main.ts**: Move setup logic out, make it just arg parsing + mode routing
47:main.ts (1100+ lines)
58:├── Agent event handling + session persistence (duplicated in main.ts)
59:├── Auto-compaction logic (duplicated in main.ts runRpcMode)
60:├── Bash execution (duplicated in main.ts)
71:├── main.ts (~200 lines)
166:- `src/main.ts`: `executeRpcBashCommand()` function (lines ~640-700)
277:- `src/main.ts`: `runRpcMode()` subscription logic (lines ~720-745)
278:- `src/main.ts`: `runSingleShotMode()` subscription logic (lines ~605-610)
674:- `src/main.ts`: `runRpcMode()` auto-compaction logic (lines ~730-770)
1145:### WP11: Create print-mode.ts
1152:- `src/main.ts`: `runSingleShotMode()` function (lines ~615-640)
1221:### WP12: Create rpc-mode.ts
1228:- `src/main.ts`: `runRpcMode()` function (lines ~700-800)
1332:### WP14: Update main.ts to use AgentSession and new modes
1333:> Refactor main.ts to use AgentSession and the new mode modules.
1336:- `src/main.ts`
1373:- [ ] Remove `runSingleShotMode()` from main.ts
1374:- [ ] Remove `runRpcMode()` from main.ts
1375:- [ ] Remove `executeRpcBashCommand()` from main.ts
1457:> Update the runInteractiveMode function in main.ts to create and pass AgentSession.
1460:- `src/main.ts`
1508:4. Update imports in main.ts
1518:- [ ] Update imports in main.ts
1555:### WP19: Extract setup logic from main.ts
1561:**Extract from main.ts:**
1616:- [ ] Move `buildSystemPrompt()` from main.ts


Let me update WP14 to create main-new.ts instead of modifying main.ts:


```typescript
// src/modes/index.ts
export { runPrintMode } from "./print-mode.js";
export { runRpcMode } from "./rpc-mode.js";
// InteractiveMode will be added later
```

- [ ] Create `src/modes/index.ts`
- [ ] Verify with `npm run check`

---

### WP14: Update main.ts to use AgentSession and new modes
> Refactor main.ts to use AgentSession and the new mode modules.

**Files to modify:**
- `src/main.ts`

**Changes:**
1. Remove `runSingleShotMode()` function (replaced by print-mode.ts)
2. Remove `runRpcMode()` function (replaced by rpc-mode.ts)
3. Remove `executeRpcBashCommand()` function (replaced by bash-executor.ts)
4. Create `AgentSession` instance after agent setup
5. Pass `AgentSession` to mode functions

**Key changes in main():**
```typescript
// After agent creation, create AgentSession
const session = new AgentSession({
  agent,
  sessionManager,
  settingsManager,
  scopedModels,
  fileCommands: loadSlashCommands(),
});

// Route to modes
if (mode === "rpc") {
  await runRpcMode(session);
} else if (isInteractive) {
  // For now, still use TuiRenderer directly (will refactor in WP15+)
  await runInteractiveMode(agent, sessionManager, ...);
} else {
  await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
}
```

**Verification:**
1. `npm run check` passes
2. Manual test: `companion -p "hello"` works
3. Manual test: `companion --mode json "hello"` works
4. Manual test: `companion --mode rpc` works

- [ ] Remove `runSingleShotMode()` from main.ts
- [ ] Remove `runRpcMode()` from main.ts
- [ ] Remove `executeRpcBashCommand()` from main.ts
- [ ] Import and use `runPrintMode` from modes
- [ ] Import and use `runRpcMode` from modes
- [ ] Create `AgentSession` in main()
- [ ] Update mode routing to use new functions
- [ ] Verify with `npm run check`
- [ ] Manual test all three modes

---

### WP15: Refactor TuiRenderer to use AgentSession
> Update TuiRenderer to use AgentSession instead of direct agent/sessionManager access.

**Files to modify:**
- `src/tui/tui-renderer.ts`

**This is the largest change. Strategy:**
1. Change constructor to accept `AgentSession` instead of separate agent/sessionManager/settingsManager
2. Replace all `this.agent.*` calls with `this.session.agent.*` or appropriate AgentSession methods
3. Replace all `this.sessionManager.*` calls with AgentSession methods
4. Replace all `this.settingsManager.*` calls with AgentSession methods where applicable
5. Remove duplicated logic that now lives in AgentSession

**Key replacements:**
| Old | New |
|-----|-----|
| `this.agent.prompt()` | `this.session.prompt()` |
| `this.agent.abort()` | `this.session.abort()` |
| `this.sessionManager.saveMessage()` | (handled internally by AgentSession.subscribe) |
| `this.cycleThinkingLevel()` | `this.session.cycleThinkingLevel()` |
| `this.cycleModel()` | `this.session.cycleModel()` |
| `this.executeBashCommand()` | `this.session.executeBash()` |
| `this.executeCompaction()` | `this.session.compact()` |
| `this.checkAutoCompaction()` | (handled internally by AgentSession) |
| `this.handleClearCommand()` reset logic | `this.session.reset()` |
| `this.handleResumeSession()` | `this.session.switchSession()` |

**Constructor change:**
```typescript
// Old
constructor(
  agent: Agent,
  sessionManager: SessionManager,
  settingsManager: SettingsManager,
  version: string,

[289 more lines in file. Use offset=1420 to continue]


edit

### WP14: Update main.ts to use AgentSession and new modes
> Refactor main.ts to use AgentSession and the new mode modules.

**Files to modify:**
- `src/main.ts`

**Changes:**
1. Remove `runSingleShotMode()` function (replaced by print-mode.ts)
2. Remove `runRpcMode()` function (replaced by rpc-mode.ts)
3. Remove `executeRpcBashCommand()` function (replaced by bash-executor.ts)
4. Create `AgentSession` instance after agent setup
5. Pass `AgentSession` to mode functions

**Key changes in main():**
```typescript
// After agent creation, create AgentSession
const session = new AgentSession({
  agent,
  sessionManager,
  settingsManager,
  scopedModels,
  fileCommands: loadSlashCommands(),
});

// Route to modes
if (mode === "rpc") {
  await runRpcMode(session);
} else if (isInteractive) {
  // For now, still use TuiRenderer directly (will refactor in WP15+)
  await runInteractiveMode(agent, sessionManager, ...);
} else {
  await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
}
```

**Verification:**
1. `npm run check` passes
2. Manual test: `companion -p "hello"` works
3. Manual test: `companion --mode json "hello"` works
4. Manual test: `companion --mode rpc` works

- [ ] Remove `runSingleShotMode()` from main.ts
- [ ] Remove `runRpcMode()` from main.ts
- [ ] Remove `executeRpcBashCommand()` from main.ts
- [ ] Import and use `runPrintMode` from modes
- [ ] Import and use `runRpcMode` from modes
- [ ] Create `AgentSession` in main()
- [ ] Update mode routing to use new functions
- [ ] Verify with `npm run check`
- [ ] Manual test all three modes

### WP14: Create main-new.ts using AgentSession and new modes
> Create a new main file that uses AgentSession and the new mode modules.
> Old main.ts is kept for reference/comparison.

**Files to create:**
- `src/main-new.ts` (copy from main.ts, then modify)
- `src/cli-new.ts` (copy from cli.ts, point to main-new.ts)

**Changes to main-new.ts:**
1. Remove `runSingleShotMode()` function (use print-mode.ts)
2. Remove `runRpcMode()` function (use rpc-mode.ts)
3. Remove `executeRpcBashCommand()` function (use bash-executor.ts)
4. Create `AgentSession` instance after agent setup
5. Pass `AgentSession` to mode functions

**Key changes in main():**
```typescript
// After agent creation, create AgentSession
const session = new AgentSession({
  agent,
  sessionManager,
  settingsManager,
  scopedModels,
  fileCommands: loadSlashCommands(),
});

// Route to modes
if (mode === "rpc") {
  await runRpcMode(session);
} else if (isInteractive) {
  // For now, still use TuiRenderer directly (will refactor in WP15+)
  await runInteractiveMode(agent, sessionManager, ...);
} else {
  await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
}
```

**cli-new.ts:**
```typescript
#!/usr/bin/env node
import { main } from "./main-new.js";
main(process.argv.slice(2));
```

**Testing the new implementation:**
```bash
# Run new implementation directly
npx tsx src/cli-new.ts -p "hello"
npx tsx src/cli-new.ts --mode json "hello"
npx tsx src/cli-new.ts  # interactive mode
```

**Verification:**
1. `npm run check` passes
2. Manual test: `npx tsx src/cli-new.ts -p "hello"` works
3. Manual test: `npx tsx src/cli-new.ts --mode json "hello"` works
4. Manual test: `npx tsx src/cli-new.ts --mode rpc` works

- [ ] Copy main.ts to main-new.ts
- [ ] Remove `runSingleShotMode()` from main-new.ts
- [ ] Remove `runRpcMode()` from main-new.ts
- [ ] Remove `executeRpcBashCommand()` from main-new.ts
- [ ] Import and use `runPrintMode` from modes
- [ ] Import and use `runRpcMode` from modes
- [ ] Create `AgentSession` in main()
- [ ] Update mode routing to use new functions
- [ ] Create cli-new.ts
- [ ] Verify with `npm run check`
- [ ] Manual test all three modes via cli-new.ts


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 1584 characters to 2202 characters.

      ...
 1328 - [ ] Verify with `npm run check`
 1329
 1330 ---
 1331
-1332 ### WP14: Update main.ts to use AgentSession and new modes
-1333 > Refactor main.ts to use AgentSession and the new mode modules.
+1332 ### WP14: Create main-new.ts using AgentSession and new modes
+1333 > Create a new main file that uses AgentSession and the new mode modules.
+1334 > Old main.ts is kept for reference/comparison.
 1334
-1335 **Files to modify:**
-1336 - `src/main.ts`
+1336 **Files to create:**
+1337 - `src/main-new.ts` (copy from main.ts, then modify)
+1338 - `src/cli-new.ts` (copy from cli.ts, point to main-new.ts)
 1337
-1338 **Changes:**
-1339 1. Remove `runSingleShotMode()` function (replaced by print-mode.ts)
-1340 2. Remove `runRpcMode()` function (replaced by rpc-mode.ts)
-1341 3. Remove `executeRpcBashCommand()` function (replaced by bash-executor.ts)
+1340 **Changes to main-new.ts:**
+1341 1. Remove `runSingleShotMode()` function (use print-mode.ts)
+1342 2. Remove `runRpcMode()` function (use rpc-mode.ts)
+1343 3. Remove `executeRpcBashCommand()` function (use bash-executor.ts)
 1342 4. Create `AgentSession` instance after agent setup
 1343 5. Pass `AgentSession` to mode functions
 1344
 1345 **Key changes in main():**
 1346 ```typescript
 1347 // After agent creation, create AgentSession
 1348 const session = new AgentSession({
 1349   agent,
 1350   sessionManager,
 1351   settingsManager,
 1352   scopedModels,
 1353   fileCommands: loadSlashCommands(),
 1354 });
 1355
 1356 // Route to modes
 1357 if (mode === "rpc") {
 1358   await runRpcMode(session);
 1359 } else if (isInteractive) {
 1360   // For now, still use TuiRenderer directly (will refactor in WP15+)
 1361   await runInteractiveMode(agent, sessionManager, ...);
 1362 } else {
 1363   await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
 1364 }
 1365 ```
 1366
+1369 **cli-new.ts:**
+1370 ```typescript
+1371 #!/usr/bin/env node
+1372 import { main } from "./main-new.js";
+1373 main(process.argv.slice(2));
+1374 ```
+1375
+1376 **Testing the new implementation:**
+1377 ```bash
+1378 # Run new implementation directly
+1379 npx tsx src/cli-new.ts -p "hello"
+1380 npx tsx src/cli-new.ts --mode json "hello"
+1381 npx tsx src/cli-new.ts  # interactive mode
+1382 ```
+1383
 1367 **Verification:**
 1368 1. `npm run check` passes
-1369 2. Manual test: `companion -p "hello"` works
-1370 3. Manual test: `companion --mode json "hello"` works
-1371 4. Manual test: `companion --mode rpc` works
+1386 2. Manual test: `npx tsx src/cli-new.ts -p "hello"` works
+1387 3. Manual test: `npx tsx src/cli-new.ts --mode json "hello"` works
+1388 4. Manual test: `npx tsx src/cli-new.ts --mode rpc` works
 1372
-1373 - [ ] Remove `runSingleShotMode()` from main.ts
-1374 - [ ] Remove `runRpcMode()` from main.ts
-1375 - [ ] Remove `executeRpcBashCommand()` from main.ts
+1390 - [ ] Copy main.ts to main-new.ts
+1391 - [ ] Remove `runSingleShotMode()` from main-new.ts
+1392 - [ ] Remove `runRpcMode()` from main-new.ts
+1393 - [ ] Remove `executeRpcBashCommand()` from main-new.ts
 1376 - [ ] Import and use `runPrintMode` from modes
 1377 - [ ] Import and use `runRpcMode` from modes
 1378 - [ ] Create `AgentSession` in main()
 1379 - [ ] Update mode routing to use new functions
+1398 - [ ] Create cli-new.ts
 1380 - [ ] Verify with `npm run check`
-1381 - [ ] Manual test all three modes
+1400 - [ ] Manual test all three modes via cli-new.ts
 1382
 1383 ---
 1384
 1385 ### WP15: Refactor TuiRenderer to use AgentSession
      ...


Now update WP15-17 to use new files instead of modifying in place:


- [ ] Verify with `npm run check`
- [ ] Manual test interactive mode thoroughly

---

### WP16: Update runInteractiveMode to use AgentSession
> Update the runInteractiveMode function in main.ts to create and pass AgentSession.

**Files to modify:**
- `src/main.ts`

**Changes:**
```typescript
async function runInteractiveMode(
  session: AgentSession,  // Changed from individual params
  version: string,
  changelogMarkdown: string | null,
  collapseChangelog: boolean,
  modelFallbackMessage: string | null,
  versionCheckPromise: Promise<string | null>,
  initialMessages: string[],
  initialMessage?: string,
  initialAttachments?: Attachment[],
  fdPath: string | null,
): Promise<void> {
  const renderer = new TuiRenderer(
    session,
    version,
    changelogMarkdown,
    collapseChangelog,
    fdPath,
  );
  // ... rest stays similar
}
```

**Verification:**
1. `npm run check` passes
2. Manual test: Interactive mode works

- [ ] Update `runInteractiveMode()` signature
- [ ] Update TuiRenderer instantiation
- [ ] Verify with `npm run check`

---

### WP17: Rename TuiRenderer to InteractiveMode
> Rename the class and file to better reflect its purpose.

**Files to rename/modify:**
- `src/tui/tui-renderer.ts` → `src/modes/interactive/interactive-mode.ts`
- Update all imports

**Steps:**
1. Create `src/modes/interactive/` directory
2. Move and rename file
3. Rename class from `TuiRenderer` to `InteractiveMode`
4. Update imports in main.ts
5. Update barrel export in modes/index.ts

**Verification:**
1. `npm run check` passes
2. Manual test: Interactive mode works

- [ ] Create `src/modes/interactive/` directory
- [ ] Move `tui/tui-renderer.ts` to `modes/interactive/interactive-mode.ts`
- [ ] Rename class to `InteractiveMode`
- [ ] Update imports in main.ts
- [ ] Update modes/index.ts barrel export
- [ ] Verify with `npm run check`

---

### WP18: Move remaining TUI components
> Move TUI-specific components to the interactive mode directory.

**Files to move:**
- `src/tui/assistant-message.ts` → `src/modes/interactive/components/`
- `src/tui/bash-execution.ts` → `src/modes/interactive/components/`
- `src/tui/compaction.ts` → `src/modes/interactive/components/`
- `src/tui/custom-editor.ts` → `src/modes/interactive/components/`
- `src/tui/dynamic-border.ts` → `src/modes/interactive/components/`
- `src/tui/footer.ts` → `src/modes/interactive/components/`
- `src/tui/model-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/oauth-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/queue-mode-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/session-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/theme-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/thinking-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/tool-execution.ts` → `src/modes/interactive/components/`
- `src/tui/user-message.ts` → `src/modes/interactive/components/`
- `src/tui/user-message-selector.ts` → `src/modes/interactive/selectors/`

**Note:** This is optional reorganization. Can be done later or skipped if too disruptive.

- [ ] Create directory structure under `src/modes/interactive/`
- [ ] Move component files
- [ ] Move selector files
- [ ] Update all imports
- [ ] Remove empty `src/tui/` directory
- [ ] Verify with `npm run check`

---

### WP19: Extract setup logic from main.ts
> Create setup.ts with model resolution, system prompt building, etc.

**Files to create:**
- `src/core/setup.ts`

**Extract from main.ts:**
- `buildSystemPrompt()` function
- `loadProjectContextFiles()` function
- `loadContextFileFromDir()` function
- `resolveModelScope()` function
- Model resolution logic (the priority system)
- Session loading/restoration logic

**Implementation:**
```typescript

[138 more lines in file. Use offset=1590 to continue]


### WP15: Refactor TuiRenderer to use AgentSession
> Update TuiRenderer to use AgentSession instead of direct agent/sessionManager access.

**Files to modify:**
- `src/tui/tui-renderer.ts`

**This is the largest change. Strategy:**
1. Change constructor to accept `AgentSession` instead of separate agent/sessionManager/settingsManager
2. Replace all `this.agent.*` calls with `this.session.agent.*` or appropriate AgentSession methods
3. Replace all `this.sessionManager.*` calls with AgentSession methods
4. Replace all `this.settingsManager.*` calls with AgentSession methods where applicable
5. Remove duplicated logic that now lives in AgentSession

**Key replacements:**
| Old | New |
|-----|-----|
| `this.agent.prompt()` | `this.session.prompt()` |
| `this.agent.abort()` | `this.session.abort()` |
| `this.sessionManager.saveMessage()` | (handled internally by AgentSession.subscribe) |
| `this.cycleThinkingLevel()` | `this.session.cycleThinkingLevel()` |
| `this.cycleModel()` | `this.session.cycleModel()` |
| `this.executeBashCommand()` | `this.session.executeBash()` |
| `this.executeCompaction()` | `this.session.compact()` |
| `this.checkAutoCompaction()` | (handled internally by AgentSession) |
| `this.handleClearCommand()` reset logic | `this.session.reset()` |
| `this.handleResumeSession()` | `this.session.switchSession()` |

**Constructor change:**
```typescript
// Old
constructor(
  agent: Agent,
  sessionManager: SessionManager,
  settingsManager: SettingsManager,
  version: string,
  ...
)

// New
constructor(
  session: AgentSession,
  version: string,
  ...
)
```

**Verification:**
1. `npm run check` passes
2. Manual test: Full interactive mode works
3. Manual test: All slash commands work
4. Manual test: All hotkeys work
5. Manual test: Bash execution works
6. Manual test: Model/thinking cycling works

- [ ] Change TuiRenderer constructor to accept AgentSession
- [ ] Update all agent access to go through session
- [ ] Remove `subscribeToAgent()` method (use session.subscribe)
- [ ] Remove `checkAutoCompaction()` method (handled by session)
- [ ] Update `cycleThinkingLevel()` to use session method
- [ ] Update `cycleModel()` to use session method
- [ ] Update bash execution to use session.executeBash()
- [ ] Update compaction to use session.compact()
- [ ] Update reset logic to use session.reset()
- [ ] Update session switching to use session.switchSession()
- [ ] Update branch logic to use session.branch()
- [ ] Remove all direct sessionManager access
- [ ] Verify with `npm run check`
- [ ] Manual test interactive mode thoroughly

---

### WP16: Update runInteractiveMode to use AgentSession
> Update the runInteractiveMode function in main.ts to create and pass AgentSession.

**Files to modify:**
- `src/main.ts`

**Changes:**
```typescript
async function runInteractiveMode(
  session: AgentSession,  // Changed from individual params
  version: string,
  changelogMarkdown: string | null,
  collapseChangelog: boolean,
  modelFallbackMessage: string | null,
  versionCheckPromise: Promise<string | null>,
  initialMessages: string[],
  initialMessage?: string,
  initialAttachments?: Attachment[],
  fdPath: string | null,
): Promise<void> {
  const renderer = new TuiRenderer(
    session,
    version,
    changelogMarkdown,
    collapseChangelog,
    fdPath,
  );
  // ... rest stays similar
}
```

**Verification:**
1. `npm run check` passes
2. Manual test: Interactive mode works

- [ ] Update `runInteractiveMode()` signature
- [ ] Update TuiRenderer instantiation
- [ ] Verify with `npm run check`

---

### WP17: Rename TuiRenderer to InteractiveMode
> Rename the class and file to better reflect its purpose.

**Files to rename/modify:**
- `src/tui/tui-renderer.ts` → `src/modes/interactive/interactive-mode.ts`
- Update all imports

**Steps:**
1. Create `src/modes/interactive/` directory
2. Move and rename file
3. Rename class from `TuiRenderer` to `InteractiveMode`
4. Update imports in main.ts
5. Update barrel export in modes/index.ts

**Verification:**
1. `npm run check` passes
2. Manual test: Interactive mode works

- [ ] Create `src/modes/interactive/` directory
- [ ] Move `tui/tui-renderer.ts` to `modes/interactive/interactive-mode.ts`
- [ ] Rename class to `InteractiveMode`
- [ ] Update imports in main.ts
- [ ] Update modes/index.ts barrel export
- [ ] Verify with `npm run check`

---

### WP18: Move remaining TUI components
> Move TUI-specific components to the interactive mode directory.

**Files to move:**
- `src/tui/assistant-message.ts` → `src/modes/interactive/components/`
- `src/tui/bash-execution.ts` → `src/modes/interactive/components/`
- `src/tui/compaction.ts` → `src/modes/interactive/components/`
- `src/tui/custom-editor.ts` → `src/modes/interactive/components/`
- `src/tui/dynamic-border.ts` → `src/modes/interactive/components/`
- `src/tui/footer.ts` → `src/modes/interactive/components/`
- `src/tui/model-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/oauth-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/queue-mode-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/session-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/theme-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/thinking-selector.ts` → `src/modes/interactive/selectors/`
- `src/tui/tool-execution.ts` → `src/modes/interactive/components/`
- `src/tui/user-message.ts` → `src/modes/interactive/components/`
- `src/tui/user-message-selector.ts` → `src/modes/interactive/selectors/`

**Note:** This is optional reorganization. Can be done later or skipped if too disruptive.

- [ ] Create directory structure under `src/modes/interactive/`
- [ ] Move component files
- [ ] Move selector files
- [ ] Update all imports
- [ ] Remove empty `src/tui/` directory
- [ ] Verify with `npm run check`

### WP15: Create InteractiveMode using AgentSession
> Create a new interactive mode class that uses AgentSession.
> Old tui-renderer.ts is kept for reference.

**Files to create:**
- `src/modes/interactive/interactive-mode.ts` (based on tui-renderer.ts)

**This is the largest change. Strategy:**
1. Copy tui-renderer.ts to new location
2. Rename class from `TuiRenderer` to `InteractiveMode`
3. Change constructor to accept `AgentSession` instead of separate agent/sessionManager/settingsManager
4. Replace all `this.agent.*` calls with `this.session.agent.*` or appropriate AgentSession methods
5. Replace all `this.sessionManager.*` calls with AgentSession methods
6. Replace all `this.settingsManager.*` calls with AgentSession methods where applicable
7. Remove duplicated logic that now lives in AgentSession

**Key replacements:**
| Old | New |
|-----|-----|
| `this.agent.prompt()` | `this.session.prompt()` |
| `this.agent.abort()` | `this.session.abort()` |
| `this.sessionManager.saveMessage()` | (handled internally by AgentSession.subscribe) |
| `this.cycleThinkingLevel()` | `this.session.cycleThinkingLevel()` |
| `this.cycleModel()` | `this.session.cycleModel()` |
| `this.executeBashCommand()` | `this.session.executeBash()` |
| `this.executeCompaction()` | `this.session.compact()` |
| `this.checkAutoCompaction()` | (handled internally by AgentSession) |
| `this.handleClearCommand()` reset logic | `this.session.reset()` |
| `this.handleResumeSession()` | `this.session.switchSession()` |

**Constructor change:**
```typescript
// Old
constructor(
  agent: Agent,
  sessionManager: SessionManager,
  settingsManager: SettingsManager,
  version: string,
  ...
)

// New
constructor(
  session: AgentSession,
  version: string,
  ...
)
```

**Verification:**
1. `npm run check` passes
2. Manual test via cli-new.ts: Full interactive mode works
3. Manual test: All slash commands work
4. Manual test: All hotkeys work
5. Manual test: Bash execution works
6. Manual test: Model/thinking cycling works

- [ ] Create `src/modes/interactive/` directory
- [ ] Copy tui-renderer.ts to interactive-mode.ts
- [ ] Rename class to `InteractiveMode`
- [ ] Change constructor to accept AgentSession
- [ ] Update all agent access to go through session
- [ ] Remove `subscribeToAgent()` method (use session.subscribe)
- [ ] Remove `checkAutoCompaction()` method (handled by session)
- [ ] Update `cycleThinkingLevel()` to use session method
- [ ] Update `cycleModel()` to use session method
- [ ] Update bash execution to use session.executeBash()
- [ ] Update compaction to use session.compact()
- [ ] Update reset logic to use session.reset()
- [ ] Update session switching to use session.switchSession()
- [ ] Update branch logic to use session.branch()
- [ ] Remove all direct sessionManager access
- [ ] Update imports to point to `../../tui/` for components (keep old components in place for now)
- [ ] Update modes/index.ts to export InteractiveMode
- [ ] Verify with `npm run check`
- [ ] Manual test interactive mode via cli-new.ts

---

### WP16: Update main-new.ts runInteractiveMode to use InteractiveMode
> Update runInteractiveMode in main-new.ts to use the new InteractiveMode class.

**Files to modify:**
- `src/main-new.ts`

**Changes:**
```typescript
import { InteractiveMode } from "./modes/interactive/interactive-mode.js";

async function runInteractiveMode(
  session: AgentSession,
  version: string,
  changelogMarkdown: string | null,
  collapseChangelog: boolean,
  modelFallbackMessage: string | null,
  versionCheckPromise: Promise<string | null>,
  initialMessages: string[],
  initialMessage?: string,
  initialAttachments?: Attachment[],
  fdPath: string | null,
): Promise<void> {
  const mode = new InteractiveMode(
    session,
    version,
    changelogMarkdown,
    collapseChangelog,
    fdPath,
  );
  // ... rest stays similar
}
```

**Verification:**
1. `npm run check` passes
2. Manual test via cli-new.ts: Interactive mode works

- [ ] Update `runInteractiveMode()` in main-new.ts
- [ ] Update InteractiveMode instantiation
- [ ] Verify with `npm run check`

---

### WP17: (OPTIONAL) Move TUI components to modes/interactive/
> Move TUI-specific components to the interactive mode directory.
> This is optional cleanup - can be skipped if too disruptive.

**Note:** The old `src/tui/` directory is kept. We just create copies/moves as needed.
For now, InteractiveMode can import from `../../tui/` to reuse existing components.

**Files to potentially move (if doing this WP):**
- `src/tui/assistant-message.ts` → `src/modes/interactive/components/`
- `src/tui/bash-execution.ts` → `src/modes/interactive/components/`
- etc.

**Skip this WP for now** - focus on getting the new architecture working first.
The component organization can be cleaned up later.

- [ ] SKIPPED (optional cleanup for later)


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 5825 characters to 4848 characters.

      ...
 1400 - [ ] Manual test all three modes via cli-new.ts
 1401
 1402 ---
 1403
-1404 ### WP15: Refactor TuiRenderer to use AgentSession
-1405 > Update TuiRenderer to use AgentSession instead of direct agent/sessionManager access.
+1404 ### WP15: Create InteractiveMode using AgentSession
+1405 > Create a new interactive mode class that uses AgentSession.
+1406 > Old tui-renderer.ts is kept for reference.
 1406
-1407 **Files to modify:**
-1408 - `src/tui/tui-renderer.ts`
+1408 **Files to create:**
+1409 - `src/modes/interactive/interactive-mode.ts` (based on tui-renderer.ts)
 1409
 1410 **This is the largest change. Strategy:**
-1411 1. Change constructor to accept `AgentSession` instead of separate agent/sessionManager/settingsManager
-1412 2. Replace all `this.agent.*` calls with `this.session.agent.*` or appropriate AgentSession methods
-1413 3. Replace all `this.sessionManager.*` calls with AgentSession methods
-1414 4. Replace all `this.settingsManager.*` calls with AgentSession methods where applicable
-1415 5. Remove duplicated logic that now lives in AgentSession
+1412 1. Copy tui-renderer.ts to new location
+1413 2. Rename class from `TuiRenderer` to `InteractiveMode`
+1414 3. Change constructor to accept `AgentSession` instead of separate agent/sessionManager/settingsManager
+1415 4. Replace all `this.agent.*` calls with `this.session.agent.*` or appropriate AgentSession methods
+1416 5. Replace all `this.sessionManager.*` calls with AgentSession methods
+1417 6. Replace all `this.settingsManager.*` calls with AgentSession methods where applicable
+1418 7. Remove duplicated logic that now lives in AgentSession
 1416
 1417 **Key replacements:**
 1418 | Old | New |
 1419 |-----|-----|
 1420 | `this.agent.prompt()` | `this.session.prompt()` |
 1421 | `this.agent.abort()` | `this.session.abort()` |
 1422 | `this.sessionManager.saveMessage()` | (handled internally by AgentSession.subscribe) |
 1423 | `this.cycleThinkingLevel()` | `this.session.cycleThinkingLevel()` |
 1424 | `this.cycleModel()` | `this.session.cycleModel()` |
 1425 | `this.executeBashCommand()` | `this.session.executeBash()` |
 1426 | `this.executeCompaction()` | `this.session.compact()` |
 1427 | `this.checkAutoCompaction()` | (handled internally by AgentSession) |
 1428 | `this.handleClearCommand()` reset logic | `this.session.reset()` |
 1429 | `this.handleResumeSession()` | `this.session.switchSession()` |
 1430
 1431 **Constructor change:**
 1432 ```typescript
 1433 // Old
 1434 constructor(
 1435   agent: Agent,
 1436   sessionManager: SessionManager,
 1437   settingsManager: SettingsManager,
 1438   version: string,
 1439   ...
 1440 )
 1441
 1442 // New
 1443 constructor(
 1444   session: AgentSession,
 1445   version: string,
 1446   ...
 1447 )
 1448 ```
 1449
 1450 **Verification:**
 1451 1. `npm run check` passes
-1452 2. Manual test: Full interactive mode works
+1455 2. Manual test via cli-new.ts: Full interactive mode works
 1453 3. Manual test: All slash commands work
 1454 4. Manual test: All hotkeys work
 1455 5. Manual test: Bash execution works
 1456 6. Manual test: Model/thinking cycling works
 1457
-1458 - [ ] Change TuiRenderer constructor to accept AgentSession
+1461 - [ ] Create `src/modes/interactive/` directory
+1462 - [ ] Copy tui-renderer.ts to interactive-mode.ts
+1463 - [ ] Rename class to `InteractiveMode`
+1464 - [ ] Change constructor to accept AgentSession
 1459 - [ ] Update all agent access to go through session
 1460 - [ ] Remove `subscribeToAgent()` method (use session.subscribe)
 1461 - [ ] Remove `checkAutoCompaction()` method (handled by session)
 1462 - [ ] Update `cycleThinkingLevel()` to use session method
 1463 - [ ] Update `cycleModel()` to use session method
 1464 - [ ] Update bash execution to use session.executeBash()
 1465 - [ ] Update compaction to use session.compact()
 1466 - [ ] Update reset logic to use session.reset()
 1467 - [ ] Update session switching to use session.switchSession()
 1468 - [ ] Update branch logic to use session.branch()
 1469 - [ ] Remove all direct sessionManager access
+1476 - [ ] Update imports to point to `../../tui/` for components (keep old components in place for now)
+1477 - [ ] Update modes/index.ts to export InteractiveMode
 1470 - [ ] Verify with `npm run check`
-1471 - [ ] Manual test interactive mode thoroughly
+1479 - [ ] Manual test interactive mode via cli-new.ts
 1472
 1473 ---
 1474
-1475 ### WP16: Update runInteractiveMode to use AgentSession
-1476 > Update the runInteractiveMode function in main.ts to create and pass AgentSession.
+1483 ### WP16: Update main-new.ts runInteractiveMode to use InteractiveMode
+1484 > Update runInteractiveMode in main-new.ts to use the new InteractiveMode class.
 1477
 1478 **Files to modify:**
-1479 - `src/main.ts`
+1487 - `src/main-new.ts`
 1480
 1481 **Changes:**
 1482 ```typescript
+1491 import { InteractiveMode } from "./modes/interactive/interactive-mode.js";
+1492
 1483 async function runInteractiveMode(
-1484   session: AgentSession,  // Changed from individual params
+1494   session: AgentSession,
 1485   version: string,
 1486   changelogMarkdown: string | null,
 1487   collapseChangelog: boolean,
 1488   modelFallbackMessage: string | null,
 1489   versionCheckPromise: Promise<string | null>,
 1490   initialMessages: string[],
 1491   initialMessage?: string,
 1492   initialAttachments?: Attachment[],
 1493   fdPath: string | null,
 1494 ): Promise<void> {
-1495   const renderer = new TuiRenderer(
+1505   const mode = new InteractiveMode(
 1496     session,
 1497     version,
 1498     changelogMarkdown,
 1499     collapseChangelog,
 1500     fdPath,
 1501   );
 1502   // ... rest stays similar
 1503 }
 1504 ```
 1505
 1506 **Verification:**
 1507 1. `npm run check` passes
-1508 2. Manual test: Interactive mode works
+1518 2. Manual test via cli-new.ts: Interactive mode works
 1509
-1510 - [ ] Update `runInteractiveMode()` signature
-1511 - [ ] Update TuiRenderer instantiation
+1520 - [ ] Update `runInteractiveMode()` in main-new.ts
+1521 - [ ] Update InteractiveMode instantiation
 1512 - [ ] Verify with `npm run check`
 1513
 1514 ---
 1515
-1516 ### WP17: Rename TuiRenderer to InteractiveMode
-1517 > Rename the class and file to better reflect its purpose.
-1518
-1519 **Files to rename/modify:**
-1520 - `src/tui/tui-renderer.ts` → `src/modes/interactive/interactive-mode.ts`
-1521 - Update all imports
-1522
-1523 **Steps:**
-1524 1. Create `src/modes/interactive/` directory
-1525 2. Move and rename file
-1526 3. Rename class from `TuiRenderer` to `InteractiveMode`
-1527 4. Update imports in main.ts
-1528 5. Update barrel export in modes/index.ts
-1529
-1530 **Verification:**
-1531 1. `npm run check` passes
-1532 2. Manual test: Interactive mode works
-1533
-1534 - [ ] Create `src/modes/interactive/` directory
-1535 - [ ] Move `tui/tui-renderer.ts` to `modes/interactive/interactive-mode.ts`
-1536 - [ ] Rename class to `InteractiveMode`
-1537 - [ ] Update imports in main.ts
-1538 - [ ] Update modes/index.ts barrel export
-1539 - [ ] Verify with `npm run check`
-1540
-1541 ---
-1542
-1543 ### WP18: Move remaining TUI components
+1526 ### WP17: (OPTIONAL) Move TUI components to modes/interactive/
 1544 > Move TUI-specific components to the interactive mode directory.
+1528 > This is optional cleanup - can be skipped if too disruptive.
 1545
-1546 **Files to move:**
+1530 **Note:** The old `src/tui/` directory is kept. We just create copies/moves as needed.
+1531 For now, InteractiveMode can import from `../../tui/` to reuse existing components.
+1532
+1533 **Files to potentially move (if doing this WP):**
 1547 - `src/tui/assistant-message.ts` → `src/modes/interactive/components/`
 1548 - `src/tui/bash-execution.ts` → `src/modes/interactive/components/`
-1549 - `src/tui/compaction.ts` → `src/modes/interactive/components/`
-1550 - `src/tui/custom-editor.ts` → `src/modes/interactive/components/`
-1551 - `src/tui/dynamic-border.ts` → `src/modes/interactive/components/`
-1552 - `src/tui/footer.ts` → `src/modes/interactive/components/`
-1553 - `src/tui/model-selector.ts` → `src/modes/interactive/selectors/`
-1554 - `src/tui/oauth-selector.ts` → `src/modes/interactive/selectors/`
-1555 - `src/tui/queue-mode-selector.ts` → `src/modes/interactive/selectors/`
-1556 - `src/tui/session-selector.ts` → `src/modes/interactive/selectors/`
-1557 - `src/tui/theme-selector.ts` → `src/modes/interactive/selectors/`
-1558 - `src/tui/thinking-selector.ts` → `src/modes/interactive/selectors/`
-1559 - `src/tui/tool-execution.ts` → `src/modes/interactive/components/`
-1560 - `src/tui/user-message.ts` → `src/modes/interactive/components/`
-1561 - `src/tui/user-message-selector.ts` → `src/modes/interactive/selectors/`
+1536 - etc.
 1562
-1563 **Note:** This is optional reorganization. Can be done later or skipped if too disruptive.
+1538 **Skip this WP for now** - focus on getting the new architecture working first.
+1539 The component organization can be cleaned up later.
 1564
-1565 - [ ] Create directory structure under `src/modes/interactive/`
-1566 - [ ] Move component files
-1567 - [ ] Move selector files
-1568 - [ ] Update all imports
-1569 - [ ] Remove empty `src/tui/` directory
-1570 - [ ] Verify with `npm run check`
+1541 - [ ] SKIPPED (optional cleanup for later)
 1571
 1572 ---
 1573
 1574 ### WP19: Extract setup logic from main.ts
      ...


Now update WP2 checkboxes and commit:

- [ ] Create `src/core/agent-session.ts` with basic structure
- [ ] Create `src/core/index.ts` barrel export
- [ ] Verify with `npm run check`

- [x] Create `src/core/agent-session.ts` with basic structure
- [x] Create `src/core/index.ts` barrel export
- [x] Verify with `npm run check`


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/docs/refactor.md. Changed 142 characters to 142 characters.

      ...
  259 **Verification:**
  260 1. `npm run check` passes
  261 2. Class can be instantiated (will test via later integration)
  262
- 263 - [ ] Create `src/core/agent-session.ts` with basic structure
- 264 - [ ] Create `src/core/index.ts` barrel export
- 265 - [ ] Verify with `npm run check`
+ 263 - [x] Create `src/core/agent-session.ts` with basic structure
+ 264 - [x] Create `src/core/index.ts` barrel export
+ 265 - [x] Verify with `npm run check`
  266
  267 ---
  268
  269 ### WP3: AgentSession - Event Subscription + Session Persistence
      ...


bash

cd /Users/badlogic/workspaces/companion-mono && git add -A && git commit -m "WP2: Create AgentSession basic structure + update plan for keep-old-code strategy"


Running formatting, linting, and type checking...

> companion-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 232 files in 119ms. No fixes applied.

> @mariozechner/companion-agent-core@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-ai@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 36 files in 41ms. No fixes applied.

> @mariozechner/companion-coding-agent@0.14.2 check
> tsgo --noEmit

> @mariozechner/companion-mom@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 12 files in 14ms. No fixes applied.

> @mariozechner/companion@0.14.2 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/companion-proxy@0.14.2 check
> biome check --write .

Checked 3 files in 4ms. No fixes applied.

> @mariozechner/companion-tui@0.14.2 check
> biome check --write . && tsgo --noEmit

Checked 22 files in 23ms. No fixes applied.

> @mariozechner/companion-web-ui@0.14.2 check
> tsc --noEmit && cd example && tsc --noEmit

> companion-web-ui-example@1.2.2 check
> tsc --noEmit

✅ All pre-commit checks passed!
[refactor 29d96ab] WP2: Create AgentSession basic structure + update plan for keep-old-code strategy
 4 files changed, 207 insertions(+), 91 deletions(-)
 create mode 100644 packages/coding-agent/src/core/agent-session.ts


Now WP3: Add event subscription with session persistence:


/**
 * AgentSession - Core abstraction for agent lifecycle and session management.
 *
 * This class is shared between all run modes (interactive, print, rpc).
 * It encapsulates:
 * - Agent state access
 * - Event subscription with automatic session persistence
 * - Model and thinking level management
 * - Compaction (manual and auto)
 * - Bash execution
 * - Session switching and branching
 *
 * Modes use this class and add their own I/O layer on top.
 */

import type { Agent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { Model } from "@mariozechner/companion-ai";
import type { SessionManager } from "../session-manager.js";
import type { SettingsManager } from "../settings-manager.js";
import type { FileSlashCommand } from "../slash-commands.js";

// ============================================================================
// Types
// ============================================================================

export interface AgentSessionConfig {
	agent: Agent;
	sessionManager: SessionManager;
	settingsManager: SettingsManager;
	/** Models to cycle through with Ctrl+P (from --models flag) */
	scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	/** File-based slash commands for expansion */
	fileCommands?: FileSlashCommand[];
}

// ============================================================================
// AgentSession Class
// ============================================================================

export class AgentSession {
	readonly agent: Agent;
	readonly sessionManager: SessionManager;
	readonly settingsManager: SettingsManager;

	private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
	private _fileCommands: FileSlashCommand[];

	constructor(config: AgentSessionConfig) {
		this.agent = config.agent;
		this.sessionManager = config.sessionManager;
		this.settingsManager = config.settingsManager;
		this._scopedModels = config.scopedModels ?? [];
		this._fileCommands = config.fileCommands ?? [];
	}

	// =========================================================================
	// Read-only State Access
	// =========================================================================

	/** Full agent state */
	get state(): AgentState {
		return this.agent.state;
	}

	/** Current model (may be null if not yet selected) */
	get model(): Model<any> | null {
		return this.agent.state.model;
	}

	/** Current thinking level */
	get thinkingLevel(): ThinkingLevel {
		return this.agent.state.thinkingLevel;
	}

	/** Whether agent is currently streaming a response */
	get isStreaming(): boolean {
		return this.agent.state.isStreaming;
	}

	/** All messages including custom types like BashExecutionMessage */
	get messages(): AppMessage[] {
		return this.agent.state.messages;
	}

	/** Current queue mode */
	get queueMode(): "all" | "one-at-a-time" {
		return this.agent.getQueueMode();
	}

	/** Current session file path */
	get sessionFile(): string {
		return this.sessionManager.getSessionFile();
	}

	/** Current session ID */
	get sessionId(): string {
		return this.sessionManager.getSessionId();
	}

	/** Scoped models for cycling (from --models flag) */
	get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel: ThinkingLevel }> {
		return this._scopedModels;
	}

	/** File-based slash commands */
	get fileCommands(): ReadonlyArray<FileSlashCommand> {
		return this._fileCommands;
	}
}


edit

import type { Agent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { Model } from "@mariozechner/companion-ai";
import type { SessionManager } from "../session-manager.js";
import type { SettingsManager } from "../settings-manager.js";
import type { FileSlashCommand } from "../slash-commands.js";

import type { Agent, AgentEvent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
import type { Model } from "@mariozechner/companion-ai";
import type { SessionManager } from "../session-manager.js";
import type { SettingsManager } from "../settings-manager.js";
import type { FileSlashCommand } from "../slash-commands.js";

/** Listener function for agent events */
export type AgentEventListener = (event: AgentEvent) => void;


Successfully replaced text in /Users/badlogic/workspaces/companion-mono/packages/coding-agent/src/core/agent-session.ts. Changed 332 characters to 449 characters.

     ...
  12  *
  13  * Modes use this class and add their own I/O layer on top.
  14  */
  15
- 16 import type { Agent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
+ 16 import type { Agent, AgentEvent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/companion-agent-core";
  17 import type { Model } from "@mariozechner/companion-ai";
  18 import type { SessionManager } from "../session-manager.js";
  19 import type { SettingsManager } from "../settings-manager.js";
  20 import type { FileSlashCommand } from "../slash-commands.js";
  21
+ 22 /** Listener function for agent events */
+ 23 export type AgentEventListener = (event: AgentEvent) => void;
+ 24
  22 // ============================================================================
  23 // Types
  24 // ============================================================================
  25
     ...