Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 73 additions & 62 deletions .claude/CLAUDE.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .claude/sessions/2026-04-11-phase0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Session 2026-04-11 — Conversation Identity Design (Phase 0)

Branch: `feature/conversation-id` (created from `origin/main`, no commits).

## Context

Prompt: `2026-04-11_245_session-identity.md` (Phase 0 of 3).

Issue #245: Every CLI run continues the same single conversation. No way to start fresh or switch between conversations. Parallel instances clobber each other.

## Done

### Investigation

Read `main.ts`, `replayHistory.ts`, `Conversation.ts`, `CommandModeState.ts`, `AppLayout.ts`, and test files.

Key findings:
- History is a single file `.sdk-history.jsonl` in cwd, hardcoded constant
- `loadHistory`/`saveHistory` are inline functions in `main.ts`
- No conversation identity exists anywhere in `claude-sdk-cli`
- `Conversation` class exposes `messages` (getter) and `setHistory(msgs)` for serialisation
- Command mode handles keys via `#handleCommandKey` in `AppLayout`, dispatching `t`/`f`/`d`/`p` plus arrows

### Design Document

Written to `~/repos/fleet/claude-fleet-shellicar/projects/claude-cli/investigation/2026-04-11_245_design.md`.

Covers:
- **ConversationManager** class: owns conversation id (UUID), maps id to history file, handles load/save/new
- **Persistence**: id in `~/.claude/sdk-conversation-id`, history in `.claude/conversations/<uuid>.jsonl`
- **Migration**: existing `.sdk-history.jsonl` adopted into new scheme on first run
- **Command mode**: `n` key for new conversation, callback from AppLayout to main.ts
- **Testing**: 5 test cases with concrete assertions, temp directory isolation

No code changed. Branch exists but has no commits.

## Next

Phase 1 (implementation) picks up from this design document.
29 changes: 29 additions & 0 deletions .claude/sessions/2026-04-11.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,32 @@ Committed: `73707b2 Add injectable runner to gatherGitSnapshot and a failing tes
**Script fix** - `~/.claude/skills/github-pr/scripts/create-github-pr.sh` was building label args via unquoted string concatenation, breaking labels with spaces (`pkg: claude-sdk-cli`). Fixed to use `set -- "$@" --label "$label"` and pass `"$@"` to `gh pr create`, which preserves quoting correctly.

**PR**: https://github.com/shellicar/claude-cli/pull/243 - auto-merge enabled, checks in progress.


## Discovery Session (fleet prompt)

Prompt: `2026-04-11_discovery.md`. No commits or PRs.

Wrote 5 component briefs to `~/repos/fleet/claude-fleet-shellicar/projects/claude-cli/briefs/`:

| Brief | Component | Key observations |
|-------|-----------|------------------|
| `claude-sdk.md` | `@shellicar/claude-sdk` | Four-block architecture (StreamProcessor, ToolRegistry, TurnRunner, QueryRunner). Blocks constructed once, reused per query. No per-query state on instances. Auth is separate (OAuth2 PKCE). |
| `claude-sdk-tools.md` | `@shellicar/claude-sdk-tools` | 15 tools, each self-contained with Zod schema. Subpath exports only. `IFileSystem` abstraction with `MemoryFileSystem` for tests. PreviewEdit/EditFile pair shares a patch store. |
| `claude-core.md` | `@shellicar/claude-core` | Terminal primitives: ANSI, input translation (30+ key types, handles Kitty/tmux/macOS), Unicode-aware reflow, viewport scrolling, config merging. No monorepo deps. |
| `claude-sdk-cli.md` | `@shellicar/claude-sdk-cli` | Active app. MVVM: State / Renderer / AppLayout. 20 test files. Depends on all three packages. esbuild single-file bundle. |
| `claude-cli.md` | `@shellicar/claude-cli` | Legacy app. Uses `@anthropic-ai/claude-agent-sdk` (not `claude-sdk`). Does NOT depend on claude-sdk or claude-sdk-tools. Has its own config, session, permissions systems. |

Filled in the build commands section of `~/repos/fleet/claude-fleet-shellicar/projects/claude-cli/README.md`.

### Old briefs safe to delete

All 6 existing briefs describe the legacy `claude-cli` app's architecture and are stale:
- `config.md` (describes the legacy app's 14-field config system)
- `core.md` (describes `ClaudeCli`, `QuerySession`, `AppState`, `PermissionManager`, `PromptManager` from the legacy app)
- `mcp-exec.md` (describes the legacy app's MCP exec integration)
- `rendering-strategy.md` (describes the legacy app's rendering)
- `rendering.md` (describes the legacy app's terminal rendering)
- `session-audit.md` (describes the legacy app's audit/session system)

These all describe `apps/claude-cli/` internals. The new `claude-cli.md` brief covers the legacy app at the right level for PM use. The detailed internals in the old briefs are no longer needed for prompt writing since no new development targets the legacy app.
31 changes: 31 additions & 0 deletions .claude/sessions/2026-04-12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Session 2026-04-12

## MVC directory structure (PR #246)

Branch: `feature/mvc-directory-structure`

Phase 1 moved source files into `model/`, `view/`, `controller/` subdirectories, updated all import paths, and added biome.json boundary enforcement. A second session verified: 427/427 tests pass, type-check passes, biome CI passes. PR #246 opened, auto-merge enabled.

The damage from the original Phase 1 session (`.sdk-history2.jsonl` committed accidentally, biome run against entire repo) was repaired before the verify session ran.

**PR #246 is open and auto-merging.** Nothing left to do here.

## ConversationStore (issue #245)

Branch: `feature/conversation-id`

Three sessions ran today on this track:

**Design revision**: Phase 0 design (single `ConversationManager` owning both identity and lifecycle) was revised. Design B chosen: single stateless `ConversationStore` class, `IFileSystem` injected, CLI holds `let conversationId`. Store provides `loadId`, `createId`, `loadHistory`, `saveHistory`, `migrate`. Path layout: id file cwd-relative (`.claude/.sdk-conversation-id`), history files home-relative (`~/.claude/conversations/<uuid>.jsonl`), legacy at `.sdk-history.jsonl`.

**Phase 1**: `ConversationStore.ts` stub + 13-test suite created. All 13 fail (not implemented). 427 other tests pass.

**Phase 2**: Implementation complete, wired into `main.ts`, `n` key added in command mode. All 440 tests pass. Pre-existing lint error in `packages/claude-core/src/reflow.ts` (noControlCharactersInRegex) not from this work.

**Phase 3 is next**: push `feature/conversation-id`, open a PR referencing #245.

Key constraint: `clearConversation()` in `AppLayout` replaces `#conversationState` with a fresh instance rather than adding `clear()` to `ConversationState`. Do not add `clear()` to tidy it up.

## MVC Phase 2 (this session)

Confirmed Phase 1 was clean from the verify session log. Added `changes.jsonl` entry to `apps/claude-sdk-cli/changes.jsonl`, pushed branch, opened PR #246 with labels `enhancement` + `pkg: claude-sdk-cli`, milestone `1.0`, reviewer `bananabot9000`, assignee `shellicar`, auto-merge enabled.
1 change: 1 addition & 0 deletions apps/claude-sdk-cli/changes.jsonl
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{"description":"Fix `GitStateMonitor` reporting the agent's own file edits and commits as human activity between turns","category":"fixed"}
{"description":"Fix `gatherGitSnapshot` crashing when any git command fails (e.g. `rev-parse HEAD` in a repo with no commits)","category":"fixed"}
{"description":"Move source files into `model/`, `view/`, and `controller/` subdirectories; add biome.json boundary enforcement","category":"changed"}
30 changes: 15 additions & 15 deletions apps/claude-sdk-cli/src/AppLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ import { sanitiseLoneSurrogates } from '@shellicar/claude-core/sanitise';
import type { Screen } from '@shellicar/claude-core/screen';
import { StdoutScreen } from '@shellicar/claude-core/screen';
import type { SdkMessageUsage } from '@shellicar/claude-sdk';
import { buildSubmitText } from './buildSubmitText.js';
import { CommandModeState } from './CommandModeState.js';
import type { Block, BlockType } from './ConversationState.js';
import { ConversationState } from './ConversationState.js';
import { readClipboardPath, readClipboardText } from './clipboard.js';
import { EditorState } from './EditorState.js';
import { logger } from './logger.js';
import { renderCommandMode } from './renderCommandMode.js';
import { buildDivider, renderBlocksToString, renderConversation } from './renderConversation.js';
import { renderEditor } from './renderEditor.js';
import { renderModel, renderStatus } from './renderStatus.js';
import { renderToolApproval } from './renderToolApproval.js';
import { StatusState } from './StatusState.js';
import type { PendingTool } from './ToolApprovalState.js';
import { ToolApprovalState } from './ToolApprovalState.js';

export type { PendingTool } from './ToolApprovalState.js';
import { buildSubmitText } from './model/buildSubmitText.js';
import { CommandModeState } from './model/CommandModeState.js';
import type { Block, BlockType } from './model/ConversationState.js';
import { ConversationState } from './model/ConversationState.js';
import { EditorState } from './model/EditorState.js';
import { StatusState } from './model/StatusState.js';
import type { PendingTool } from './model/ToolApprovalState.js';
import { ToolApprovalState } from './model/ToolApprovalState.js';
import { renderCommandMode } from './view/renderCommandMode.js';
import { buildDivider, renderBlocksToString, renderConversation } from './view/renderConversation.js';
import { renderEditor } from './view/renderEditor.js';
import { renderModel, renderStatus } from './view/renderStatus.js';
import { renderToolApproval } from './view/renderToolApproval.js';

export type { PendingTool } from './model/ToolApprovalState.js';

type Mode = 'editor' | 'streaming';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { relative } from 'node:path';
import type { MessagePort } from 'node:worker_threads';
import { CacheTtl, calculateCost, type DurableConfig, type SdkMessage, type SdkMessageUsage, type SdkToolApprovalRequest } from '@shellicar/claude-sdk';
import type { RefStore } from '@shellicar/claude-sdk-tools/RefStore';
import type { AppLayout, PendingTool } from './AppLayout.js';
import type { logger } from './logger.js';
import { getPermission, PermissionAction } from './permissions.js';
import type { AppLayout, PendingTool } from '../AppLayout.js';
import type { logger } from '../logger.js';
import { getPermission, PermissionAction } from '../permissions.js';

// ---- helpers (moved from runAgent.ts) ------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/src/entry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import { createRef } from '@shellicar/claude-sdk-tools/Ref';
import { RefStore } from '@shellicar/claude-sdk-tools/RefStore';
import { SearchFiles } from '@shellicar/claude-sdk-tools/SearchFiles';
import { Tail } from '@shellicar/claude-sdk-tools/Tail';
import { AgentMessageHandler } from '../AgentMessageHandler.js';
import { AppLayout } from '../AppLayout.js';
import { ClaudeMdLoader } from '../ClaudeMdLoader.js';
import { initConfig } from '../cli-config/initConfig.js';
import { SdkConfigWatcher } from '../cli-config/SdkConfigWatcher.js';
import { AgentMessageHandler } from '../controller/AgentMessageHandler.js';
import { GitStateMonitor } from '../GitStateMonitor.js';
import { printUsage, printVersion, printVersionInfo, startupBannerText } from '../help.js';
import { logger } from '../logger.js';
Expand Down
17 changes: 17 additions & 0 deletions apps/claude-sdk-cli/src/model/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"root": false,
"extends": "//",
"linter": {
"rules": {
"style": {
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {},
"patterns": [{ "group": ["../view/*"], "message": "model/ must not import from view/" }, { "group": ["../controller/*"], "message": "model/ must not import from controller/" }]
}
}
}
}
}
}
17 changes: 17 additions & 0 deletions apps/claude-sdk-cli/src/view/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"root": false,
"extends": "//",
"linter": {
"rules": {
"style": {
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {},
"patterns": [{ "group": ["../controller/*"], "message": "view/ must not import from controller/" }]
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { basename } from 'node:path';
import { DIM, INVERSE_OFF, INVERSE_ON, RESET } from '@shellicar/claude-core/ansi';
import { wrapLine } from '@shellicar/claude-core/reflow';
import { StatusLineBuilder } from '@shellicar/claude-core/status-line';
import type { CommandModeState } from './CommandModeState.js';
import type { CommandModeState } from '../model/CommandModeState.js';

// Same indent used by renderConversation for block content lines.
const CONTENT_INDENT = ' ';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DIM, RESET } from '@shellicar/claude-core/ansi';
import { wrapLine } from '@shellicar/claude-core/reflow';
import { highlight, supportsLanguage } from 'cli-highlight';
import type { Block, ConversationState } from './ConversationState.js';
import type { Block, ConversationState } from '../model/ConversationState.js';

const FILL = '\u2500';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { INVERSE_OFF, INVERSE_ON } from '@shellicar/claude-core/ansi';
import { wrapLine } from '@shellicar/claude-core/reflow';
import type { EditorState } from './EditorState.js';
import type { EditorState } from '../model/EditorState.js';

/**
* Render the editor text content for the current state.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RESET, YELLOW } from '@shellicar/claude-core/ansi';
import { StatusLineBuilder } from '@shellicar/claude-core/status-line';
import type { StatusState } from './StatusState.js';
import type { StatusState } from '../model/StatusState.js';

/**
* Extracts the model family name and capitalises it.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { wrapLine } from '@shellicar/claude-core/reflow';
import type { ToolApprovalState } from './ToolApprovalState.js';
import type { ToolApprovalState } from '../model/ToolApprovalState.js';

const CONTENT_INDENT = ' ';

Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { MessageChannel } from 'node:worker_threads';
import { type AnyToolDefinition, CacheTtl, type DurableConfig } from '@shellicar/claude-sdk';
import { describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { AgentMessageHandler, type AgentMessageHandlerOptions } from '../src/AgentMessageHandler.js';
import type { AppLayout } from '../src/AppLayout.js';
import { AgentMessageHandler, type AgentMessageHandlerOptions } from '../src/controller/AgentMessageHandler.js';
import { logger } from '../src/logger.js';

// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/CommandModeState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { CommandModeState } from '../src/CommandModeState.js';
import { CommandModeState } from '../src/model/CommandModeState.js';

describe('CommandModeState — initial state', () => {
it('commandMode starts false', () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/ConversationState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ConversationState } from '../src/ConversationState.js';
import { ConversationState } from '../src/model/ConversationState.js';

describe('ConversationState — initial state', () => {
it('sealedBlocks starts empty', () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/EditorState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { EditorState } from '../src/EditorState.js';
import { EditorState } from '../src/model/EditorState.js';

// ---------------------------------------------------------------------------
// Helpers
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/StatusState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { StatusState } from '../src/StatusState.js';
import { StatusState } from '../src/model/StatusState.js';

function makeUsage(inputTokens: number, opts: { cacheCreation?: number; cacheRead?: number; output?: number; cost?: number; contextWindow?: number } = {}): Parameters<StatusState['update']>[0] {
return {
Expand Down
2 changes: 1 addition & 1 deletion apps/claude-sdk-cli/test/ToolApprovalState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ToolApprovalState } from '../src/ToolApprovalState.js';
import { ToolApprovalState } from '../src/model/ToolApprovalState.js';

const toolA = { requestId: 'a', name: 'read_file', input: { path: '/tmp/foo' } };
const toolB = { requestId: 'b', name: 'write_file', input: { path: '/tmp/bar', content: 'hi' } };
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/buildSubmitText.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { Attachment } from '../src/AttachmentStore.js';
import { buildSubmitText } from '../src/buildSubmitText.js';
import type { Attachment } from '../src/model/AttachmentStore.js';
import { buildSubmitText } from '../src/model/buildSubmitText.js';

// ---------------------------------------------------------------------------
// No attachments
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/renderCommandMode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { CommandModeState } from '../src/CommandModeState.js';
import { renderCommandMode } from '../src/renderCommandMode.js';
import { CommandModeState } from '../src/model/CommandModeState.js';
import { renderCommandMode } from '../src/view/renderCommandMode.js';

const COLS = 120;
const MAX_TEXT_LINES = 8;
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/renderConversation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { ConversationState } from '../src/ConversationState.js';
import { buildDivider, renderConversation } from '../src/renderConversation.js';
import { ConversationState } from '../src/model/ConversationState.js';
import { buildDivider, renderConversation } from '../src/view/renderConversation.js';

// Strip ANSI escape codes so assertions can match plain text
function stripAnsi(s: string): string {
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/renderEditor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INVERSE_ON } from '@shellicar/claude-core/ansi';
import { describe, expect, it } from 'vitest';
import { EditorState } from '../src/EditorState.js';
import { renderEditor } from '../src/renderEditor.js';
import { EditorState } from '../src/model/EditorState.js';
import { renderEditor } from '../src/view/renderEditor.js';

// ---------------------------------------------------------------------------
// Helpers
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/renderStatus.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { renderModel, renderStatus } from '../src/renderStatus.js';
import { StatusState } from '../src/StatusState.js';
import { StatusState } from '../src/model/StatusState.js';
import { renderModel, renderStatus } from '../src/view/renderStatus.js';

function makeState(inputTokens: number, opts: { cacheCreation?: number; cacheRead?: number; output?: number; cost?: number; contextWindow?: number } = {}): StatusState {
const state = new StatusState();
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/test/renderToolApproval.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { renderToolApproval } from '../src/renderToolApproval.js';
import { ToolApprovalState } from '../src/ToolApprovalState.js';
import { ToolApprovalState } from '../src/model/ToolApprovalState.js';
import { renderToolApproval } from '../src/view/renderToolApproval.js';

const COLS = 120;
const MAX_ROWS = 10;
Expand Down
Loading