diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 998fc83..f68afb1 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,67 +2,52 @@ # @shellicar/claude-cli — Repo Memory - -## Why This Harness Exists + +## Identity -Each session starts with a blank slate. You have no memory of previous sessions, no recollection of what was built, what broke, what decisions were made. This is the fundamental challenge: complex work spans many sessions, but each session begins from zero. +You are a worker. Your job is one cast — one task, one repository, one goal. -Without structure, two failure patterns emerge. First, trying to do too much at once, attempting to implement everything in a single pass, running out of context mid-implementation, and leaving the next session with half-built, undocumented work to untangle. Second, looking around at existing progress and prematurely concluding the work is done. +Each cast is its own clean shot at success. If something doesn't land, only that cast needs to be re-run — nothing built after it is affected. -The harness and session logs exist to solve this. They are your memory across sessions: the mechanism that turns disconnected sessions into continuous progress. +Even if you don't reach the goal, what you leave behind is just as valuable. Every approach you tried, every path you explored — written clearly for whoever comes next. The context disappears when this cast ends. What you write does not. This is your testament. -**How the pattern works:** +The fleet has four roles: -- **On start**: Read the harness and recent session logs to understand current state, architecture, conventions, and what was last worked on. This is how you "get up to speed", the same way an engineer reads handoff notes at the start of a shift. -- **During work**: Work on one thing at a time. Finish it, verify it works, commit it in a clean state. A clean state means code that another session could pick up without first having to untangle a mess. Descriptive commit messages and progress notes create recovery points. If something goes wrong, there is a known-good state to return to. -- **On finish**: Record what you did, what state things are in, and what comes next. This is the handoff note for the next session. Without it, the next session wastes time re-discovering context instead of making progress. +- **Fleet Manager (FM)**: maintains the templates and tooling that reach you through this harness. Your operating environment comes from the FM. +- **Project Manager (PM)**: investigated the problem and distilled the findings into your prompt. +- **Worker**: you. One cast, one task, one repository, one goal. +- **Supervisor**: verifies the outcome of each cast before the next one starts. Currently the Supreme Commander. + -**Why incremental progress matters**: Working on one feature at a time and verifying it before moving on prevents the cascading failures that come from broad, shallow implementation. It also means each commit represents a working state of the codebase. + +## Your Testament -**Why verification matters**: Code changes that look correct may not work end-to-end. Verify that a feature actually works as a user would experience it before considering it complete. Bugs caught during implementation are cheap; bugs discovered sessions later (when context is lost) are expensive. +The work you do in this cast matters. What you discover along the way matters more. -The harness is deliberately structured. The architecture section, conventions, and current state are not documentation for its own sake. They are the minimum context needed to do useful work without re-exploring the entire codebase each session. - +Most prompts span multiple casts. The knowledge you build up during a cast disappears when it ends. Your testament is how it survives. - -## Never Guess +**Mechanics** -If you do not have enough information to do something, stop and ask. Do not guess. Do not infer. Do not fill in blanks with what seems reasonable. +Run `date '+%Y-%m-%d %H:%M'` to get the current time. -This applies to everything: requirements, API behavior, architectural decisions, file locations, conventions, git state, file contents, whether a change is related to your work. If you are not certain, you do not know. Act accordingly. +At the start of your cast, read previous testaments. They are the context you don't have. -**Guessing includes not looking.** If you have not checked git status, you do not know what files have changed. If you have not read a file, you do not know what it contains. If you have not verified a build or test output, you do not know whether your changes work. Assuming something is true without checking is a guess. Dismissing something as unrelated without reading it is a guess. Every tool you have exists so you do not need to guess. Use them. +At the end of your cast, or at a significant milestone, write in your testament. The file is `.claude/testament/YYYY-MM-DD.md`. If it exists, append at the bottom. If it doesn't, create it. Format each entry with the time as the header: -Guessing is poison. A guessed assumption becomes a code decision. Other code builds on that decision. Future sessions read that code and treat it as intentional. By the time the error surfaces, it has compounded across commits, sessions, and hours of wasted time. The damage is never contained to the guess itself: it spreads to everything downstream. - -A question costs one message. A look costs one tool call. A guess costs everything built on top of it. - - - -## Session Protocol - -Every session has three phases: start, work, end. - -### Session Start +``` +# HH:mm +``` -1. Read this file -2. Find recent session logs: `find .claude/sessions -name '*.md' 2>/dev/null | sort -r | head -5` -3. Read session logs found. Understand current state before doing anything. -4. Create or switch to the correct branch (if specified in prompt) -5. Build your TODO list from the prompt, present it before starting work +The git log records what happened. The code shows what exists. Your testament is everything else — the understanding that would otherwise disappear when this cast ends. -### Work +**What to write** -- Work one task at a time. Mark each in-progress, then completed. -- If a task is dropped, mark it `[-]` with a brief reason +Think about what helped you from reading previous testaments — write more of that. -### Session End +Think about what didn't help — don't write that. -1. Write a session log to `.claude/sessions/YYYY-MM-DD.md` covering what was done, what changed, decisions made, and what's next. Auto-memory is for transient context about the user. Session logs are for things the project needs to remember: they are version-controlled and visible to every future session. -2. Update `Current State` below if branch or in-progress work changed -3. Update `Recent Decisions` below if you made an architectural decision -4. Commit session log and state updates together - +Write what you know that the code doesn't say. + ## Current State @@ -115,7 +100,6 @@ If a design decision serves none of the pillars, it probably doesn't belong in t Full detail: `.claude/five-banana-pillars.md` - ## Architecture @@ -132,30 +116,55 @@ Full detail: `.claude/five-banana-pillars.md` | `packages/claude-core/` | Shared ANSI/terminal utilities: `sanitise`, `reflow`, `screen`, `status-line`, `viewport`, `renderer` | | `packages/typescript-config/` | Shared tsconfig base | +### TUI Architecture (MVC + MVVM) + +The TUI in `apps/claude-sdk-cli/` has four distinct roles. These map to classic MVC and MVVM patterns. + +**Model/State** — Pure data and transitions. No rendering, no I/O. Each state object is a pure state machine. State is owned by whoever is responsible for updating it. State objects are created by the DI container (`main.ts`) and passed to whoever needs to read or write them. + +Examples: `EditorState`, `CommandModeState`, `ConversationState`, `ToolApprovalState`, `StatusState`, `ConversationSession`. + +**ViewModel/Renderer** — Pure functions: `(state, cols) → string | string[]`. Given state and terminal width, produce display strings. No side effects, no screen knowledge. + +Examples: `renderEditor`, `renderCommandMode`, `renderConversation`, `renderToolApproval`, `renderStatus`. + +**View/Display** — Screen output only. Owns the physical screen (alt buffer, cursor positioning, writes). Calls renderers, assembles rows, writes to terminal. Reads from state, never writes to it. + +**Controller/Input** — Routes keypresses to state objects. Holds async promises (`waitForInput`, `requestApproval`). Writes to state via state object methods, never touches the screen. + +`main.ts` is the DI container: creates state objects, creates View and Controller, passes state references to both, wires external event sources (config watcher, SDK stream handler) to the appropriate state objects. + +**The constraint**: state is never updated by routing through the View. The View reads state. The Controller writes state. They share references to the same state objects but do not depend on each other. + +**Current status**: `AppLayout` currently combines View and Controller into one class and owns state internally. The state classes and renderer functions were extracted (refactor steps 1a through 5d) but were never moved out of `AppLayout`. Splitting View from Controller and externalising state ownership is planned. + ### Key files in `apps/claude-sdk-cli/src/` | File | Role | |------|------| -| `entry/main.ts` | Entry point: creates agent, layout, starts readline loop | -| `AppLayout.ts` | TUI: full cursor editor, streaming display, compaction blocks, tool approval, command mode, attachment chips | -| `AttachmentStore.ts` | `TextAttachment \| FileAttachment` union; SHA-256 dedup; 10 KB text cap; `addFile(path, kind, size?)` | -| `clipboard.ts` | `readClipboardText()`; three-stage `readClipboardPath()` (pbpaste → VS Code code/file-list JXA → osascript furl); `looksLikePath`; `sanitiseFurlResult` | -| `runAgent.ts` | Wires agent to layout: sets up tools, beta flags, event handlers | -| File | Role | -|------|------| -| `entry/main.ts` | Entry point: creates agent, layout, starts readline loop | -| `AppLayout.ts` | TUI: full cursor editor, streaming display, compaction blocks, tool approval, command mode, attachment chips | -| `AttachmentStore.ts` | `TextAttachment \| FileAttachment` union; SHA-256 dedup; 10 KB text cap; `addFile(path, kind, size?)` | -| `clipboard.ts` | `readClipboardText()`; three-stage `readClipboardPath()` (pbpaste → VS Code code/file-list JXA → osascript furl); `looksLikePath`; `sanitiseFurlResult` | +| `entry/main.ts` | DI container: creates state, agent, layout, wires everything, runs main loop | +| `AppLayout.ts` | Combined View+Controller (to be separated): screen output, key routing, state ownership | | `EditorState.ts` | Pure editor state + `handleKey(key): boolean` transitions. No rendering, no I/O. | -| `renderEditor.ts` | Pure `renderEditor(state: EditorState, cols: number): string[]` renderer. | -| `StatusState.ts` | Token/cost accumulators: 7 fields, single `update(msg)` method. Pure state. | -| `renderStatus.ts` | Pure `renderStatus(state: StatusState, cols: number): string` renderer. | -| `AgentMessageHandler.ts` | Maps all `SdkMessage` events → layout calls / state mutations. Extracted from `runAgent.ts`. | -| `runAgent.ts` | Wires agent to layout: sets up tools, beta flags, constructs handler, wires `port.on` | +| `CommandModeState.ts` | Command mode flag, attachment store, preview state. No rendering. | +| `ConversationState.ts` | Sealed blocks, active block, flush boundary. No rendering. | +| `ToolApprovalState.ts` | Pending tools, selection, approval promise queue. No rendering. | +| `StatusState.ts` | Token/cost accumulators + model name. Pure state. | +| `renderEditor.ts` | Pure `renderEditor(state, cols): string[]` | +| `renderCommandMode.ts` | Pure `renderCommandMode(state, cols, ...): { commandRow, previewRows }` | +| `renderConversation.ts` | Pure `renderConversation(state, cols): string[]` | +| `renderToolApproval.ts` | Pure `renderToolApproval(state, cols, maxRows): { approvalRow, expandedRows }` | +| `renderStatus.ts` | Pure `renderModel(state, cols): string` and `renderStatus(state, cols): string` | +| `AgentMessageHandler.ts` | Maps `SdkMessage` events → state mutations / layout calls | +| `runAgent.ts` | Wires agent to layout: tools, beta flags, handler, `port.on` | +| `AttachmentStore.ts` | `TextAttachment \| FileAttachment` union; SHA-256 dedup; 10 KB text cap | +| `clipboard.ts` | `readClipboardText()`; three-stage `readClipboardPath()` | | `permissions.ts` | Tool auto-approve/deny rules | | `redact.ts` | Strips sensitive values from tool inputs before display | | `logger.ts` | Winston file logger (`claude-sdk-cli.log`) | + +### Key files in `packages/claude-sdk/src/` + +| File | Role | |------|------| | `public/interfaces.ts` | `IAnthropicAgent` abstract class (public contract) | | `public/types.ts` | `RunAgentQuery`, `SdkMessage` union, tool types | @@ -206,7 +215,7 @@ Release markers: `{"type":"release","version":"1.0.0-beta.1","date":"YYYY-MM-DD" - **Formatter/linter**: `biome` - **Git hooks**: `lefthook` — runs biome on commit -- **Fix command**: `pnpm biome check --diagnostic-level=error --write` +- **Fix command**: `pnpm ci:fix` — NEVER use `pnpm biome check --write` directly; it runs against the entire repo and will modify files outside your scope - If biome reports only **unsafe** fixes, do NOT use `--write --unsafe` — fix manually - Do NOT hand-edit formatting — use biome. Hand fixes waste time and are often wrong - **Type check**: `pnpm type-check` @@ -265,8 +274,9 @@ Opt-in via `shellicarMcp: true` config. Registers an in-process MCP server (`she 9. **No atomic session file writes** — `writeFileSync` is not atomic. Crash during write corrupts `.claude/cli-session`. + -- **MVVM architecture refactor** (2026-04-06): Three-layer model — State (pure data + transitions), Renderer (pure `(state, cols) → string[]`), ScreenCoordinator (owns screen, routes events, calls renderers). Pull-based: coordinator decides when to render. Plan in `.claude/plans/architecture-refactor.md`. Enables unit testing of state and render logic without terminal knowledge. +- **TUI architecture (MVC + MVVM)** (2026-04-06, clarified 2026-04-11): Four roles: Model/State (pure data, owned by whoever updates it), ViewModel/Renderer (pure functions), View/Display (screen output only, reads state), Controller/Input (key routing, writes state). State is never updated by routing through the View. `main.ts` is the DI container. `AppLayout` currently combines View + Controller + state ownership; separation is planned. The state and renderer layers were correctly extracted (steps 1a–5d) but never moved out of AppLayout. See `Architecture > TUI Architecture` section above. Design doc: `projects/claude-cli/investigation/2026-04-11_245_design-v2.md` in fleet repo. - **`PreviewEdit` input schema** (2026-04-08): Two separate arrays instead of one flat `edits` array. - `lineEdits`: `insert | replace | delete` — all line numbers reference the file **before the call**. The tool sorts bottom-to-top internally so earlier edits never shift later targets. Safe to specify in any order. - `textEdits`: `replace_text | regex_text` — applied in order, **after** all `lineEdits` complete. They see the post-`lineEdits` content, not the original. @@ -282,4 +292,5 @@ Opt-in via `shellicarMcp: true` config. Registers an in-process MCP server (`she + diff --git a/.claude/sessions/2026-04-11-phase0.md b/.claude/sessions/2026-04-11-phase0.md new file mode 100644 index 0000000..59dc72f --- /dev/null +++ b/.claude/sessions/2026-04-11-phase0.md @@ -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/.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. diff --git a/.claude/sessions/2026-04-11.md b/.claude/sessions/2026-04-11.md index 51c0b12..35f3fd3 100644 --- a/.claude/sessions/2026-04-11.md +++ b/.claude/sessions/2026-04-11.md @@ -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. \ No newline at end of file diff --git a/.claude/sessions/2026-04-12.md b/.claude/sessions/2026-04-12.md new file mode 100644 index 0000000..10cb6b8 --- /dev/null +++ b/.claude/sessions/2026-04-12.md @@ -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/.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. diff --git a/apps/claude-sdk-cli/changes.jsonl b/apps/claude-sdk-cli/changes.jsonl index 2e472b7..02125fe 100644 --- a/apps/claude-sdk-cli/changes.jsonl +++ b/apps/claude-sdk-cli/changes.jsonl @@ -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"} diff --git a/apps/claude-sdk-cli/src/AppLayout.ts b/apps/claude-sdk-cli/src/AppLayout.ts index 34a48c0..1395caa 100644 --- a/apps/claude-sdk-cli/src/AppLayout.ts +++ b/apps/claude-sdk-cli/src/AppLayout.ts @@ -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'; diff --git a/apps/claude-sdk-cli/src/AgentMessageHandler.ts b/apps/claude-sdk-cli/src/controller/AgentMessageHandler.ts similarity index 98% rename from apps/claude-sdk-cli/src/AgentMessageHandler.ts rename to apps/claude-sdk-cli/src/controller/AgentMessageHandler.ts index fda36e7..776840f 100644 --- a/apps/claude-sdk-cli/src/AgentMessageHandler.ts +++ b/apps/claude-sdk-cli/src/controller/AgentMessageHandler.ts @@ -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) ------------------------------------ diff --git a/apps/claude-sdk-cli/src/entry/main.ts b/apps/claude-sdk-cli/src/entry/main.ts index 7bbf80f..423806e 100644 --- a/apps/claude-sdk-cli/src/entry/main.ts +++ b/apps/claude-sdk-cli/src/entry/main.ts @@ -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'; diff --git a/apps/claude-sdk-cli/src/AttachmentStore.ts b/apps/claude-sdk-cli/src/model/AttachmentStore.ts similarity index 100% rename from apps/claude-sdk-cli/src/AttachmentStore.ts rename to apps/claude-sdk-cli/src/model/AttachmentStore.ts diff --git a/apps/claude-sdk-cli/src/CommandModeState.ts b/apps/claude-sdk-cli/src/model/CommandModeState.ts similarity index 100% rename from apps/claude-sdk-cli/src/CommandModeState.ts rename to apps/claude-sdk-cli/src/model/CommandModeState.ts diff --git a/apps/claude-sdk-cli/src/ConversationState.ts b/apps/claude-sdk-cli/src/model/ConversationState.ts similarity index 100% rename from apps/claude-sdk-cli/src/ConversationState.ts rename to apps/claude-sdk-cli/src/model/ConversationState.ts diff --git a/apps/claude-sdk-cli/src/EditorState.ts b/apps/claude-sdk-cli/src/model/EditorState.ts similarity index 100% rename from apps/claude-sdk-cli/src/EditorState.ts rename to apps/claude-sdk-cli/src/model/EditorState.ts diff --git a/apps/claude-sdk-cli/src/StatusState.ts b/apps/claude-sdk-cli/src/model/StatusState.ts similarity index 100% rename from apps/claude-sdk-cli/src/StatusState.ts rename to apps/claude-sdk-cli/src/model/StatusState.ts diff --git a/apps/claude-sdk-cli/src/ToolApprovalState.ts b/apps/claude-sdk-cli/src/model/ToolApprovalState.ts similarity index 100% rename from apps/claude-sdk-cli/src/ToolApprovalState.ts rename to apps/claude-sdk-cli/src/model/ToolApprovalState.ts diff --git a/apps/claude-sdk-cli/src/model/biome.json b/apps/claude-sdk-cli/src/model/biome.json new file mode 100644 index 0000000..5e4f41a --- /dev/null +++ b/apps/claude-sdk-cli/src/model/biome.json @@ -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/" }] + } + } + } + } + } +} diff --git a/apps/claude-sdk-cli/src/buildSubmitText.ts b/apps/claude-sdk-cli/src/model/buildSubmitText.ts similarity index 100% rename from apps/claude-sdk-cli/src/buildSubmitText.ts rename to apps/claude-sdk-cli/src/model/buildSubmitText.ts diff --git a/apps/claude-sdk-cli/src/view/biome.json b/apps/claude-sdk-cli/src/view/biome.json new file mode 100644 index 0000000..eab7d19 --- /dev/null +++ b/apps/claude-sdk-cli/src/view/biome.json @@ -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/" }] + } + } + } + } + } +} diff --git a/apps/claude-sdk-cli/src/renderCommandMode.ts b/apps/claude-sdk-cli/src/view/renderCommandMode.ts similarity index 98% rename from apps/claude-sdk-cli/src/renderCommandMode.ts rename to apps/claude-sdk-cli/src/view/renderCommandMode.ts index fea5a27..093e30f 100644 --- a/apps/claude-sdk-cli/src/renderCommandMode.ts +++ b/apps/claude-sdk-cli/src/view/renderCommandMode.ts @@ -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 = ' '; diff --git a/apps/claude-sdk-cli/src/renderConversation.ts b/apps/claude-sdk-cli/src/view/renderConversation.ts similarity index 98% rename from apps/claude-sdk-cli/src/renderConversation.ts rename to apps/claude-sdk-cli/src/view/renderConversation.ts index aeea77c..8482fa0 100644 --- a/apps/claude-sdk-cli/src/renderConversation.ts +++ b/apps/claude-sdk-cli/src/view/renderConversation.ts @@ -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'; diff --git a/apps/claude-sdk-cli/src/renderEditor.ts b/apps/claude-sdk-cli/src/view/renderEditor.ts similarity index 95% rename from apps/claude-sdk-cli/src/renderEditor.ts rename to apps/claude-sdk-cli/src/view/renderEditor.ts index 86943cf..a0f0e6a 100644 --- a/apps/claude-sdk-cli/src/renderEditor.ts +++ b/apps/claude-sdk-cli/src/view/renderEditor.ts @@ -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. diff --git a/apps/claude-sdk-cli/src/renderStatus.ts b/apps/claude-sdk-cli/src/view/renderStatus.ts similarity index 97% rename from apps/claude-sdk-cli/src/renderStatus.ts rename to apps/claude-sdk-cli/src/view/renderStatus.ts index de9f945..5b6e946 100644 --- a/apps/claude-sdk-cli/src/renderStatus.ts +++ b/apps/claude-sdk-cli/src/view/renderStatus.ts @@ -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. diff --git a/apps/claude-sdk-cli/src/renderToolApproval.ts b/apps/claude-sdk-cli/src/view/renderToolApproval.ts similarity index 95% rename from apps/claude-sdk-cli/src/renderToolApproval.ts rename to apps/claude-sdk-cli/src/view/renderToolApproval.ts index 766e63a..5d56aba 100644 --- a/apps/claude-sdk-cli/src/renderToolApproval.ts +++ b/apps/claude-sdk-cli/src/view/renderToolApproval.ts @@ -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 = ' '; diff --git a/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts b/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts index 7b036dd..73999de 100644 --- a/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts +++ b/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts @@ -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'; // --------------------------------------------------------------------------- diff --git a/apps/claude-sdk-cli/test/CommandModeState.spec.ts b/apps/claude-sdk-cli/test/CommandModeState.spec.ts index 81ef755..1e58fa0 100644 --- a/apps/claude-sdk-cli/test/CommandModeState.spec.ts +++ b/apps/claude-sdk-cli/test/CommandModeState.spec.ts @@ -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', () => { diff --git a/apps/claude-sdk-cli/test/ConversationState.spec.ts b/apps/claude-sdk-cli/test/ConversationState.spec.ts index 06b9365..ef1b448 100644 --- a/apps/claude-sdk-cli/test/ConversationState.spec.ts +++ b/apps/claude-sdk-cli/test/ConversationState.spec.ts @@ -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', () => { diff --git a/apps/claude-sdk-cli/test/EditorState.spec.ts b/apps/claude-sdk-cli/test/EditorState.spec.ts index 1f0fcb8..2c84685 100644 --- a/apps/claude-sdk-cli/test/EditorState.spec.ts +++ b/apps/claude-sdk-cli/test/EditorState.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { EditorState } from '../src/EditorState.js'; +import { EditorState } from '../src/model/EditorState.js'; // --------------------------------------------------------------------------- // Helpers diff --git a/apps/claude-sdk-cli/test/StatusState.spec.ts b/apps/claude-sdk-cli/test/StatusState.spec.ts index 8ef7acc..0e511d4 100644 --- a/apps/claude-sdk-cli/test/StatusState.spec.ts +++ b/apps/claude-sdk-cli/test/StatusState.spec.ts @@ -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[0] { return { diff --git a/apps/claude-sdk-cli/test/ToolApprovalState.spec.ts b/apps/claude-sdk-cli/test/ToolApprovalState.spec.ts index f3c3a18..74160d8 100644 --- a/apps/claude-sdk-cli/test/ToolApprovalState.spec.ts +++ b/apps/claude-sdk-cli/test/ToolApprovalState.spec.ts @@ -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' } }; diff --git a/apps/claude-sdk-cli/test/buildSubmitText.spec.ts b/apps/claude-sdk-cli/test/buildSubmitText.spec.ts index 27499bd..d09834a 100644 --- a/apps/claude-sdk-cli/test/buildSubmitText.spec.ts +++ b/apps/claude-sdk-cli/test/buildSubmitText.spec.ts @@ -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 diff --git a/apps/claude-sdk-cli/test/renderCommandMode.spec.ts b/apps/claude-sdk-cli/test/renderCommandMode.spec.ts index e51710a..33be180 100644 --- a/apps/claude-sdk-cli/test/renderCommandMode.spec.ts +++ b/apps/claude-sdk-cli/test/renderCommandMode.spec.ts @@ -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; diff --git a/apps/claude-sdk-cli/test/renderConversation.spec.ts b/apps/claude-sdk-cli/test/renderConversation.spec.ts index 7331c61..3d19a0f 100644 --- a/apps/claude-sdk-cli/test/renderConversation.spec.ts +++ b/apps/claude-sdk-cli/test/renderConversation.spec.ts @@ -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 { diff --git a/apps/claude-sdk-cli/test/renderEditor.spec.ts b/apps/claude-sdk-cli/test/renderEditor.spec.ts index e33dd80..3b75d95 100644 --- a/apps/claude-sdk-cli/test/renderEditor.spec.ts +++ b/apps/claude-sdk-cli/test/renderEditor.spec.ts @@ -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 diff --git a/apps/claude-sdk-cli/test/renderStatus.spec.ts b/apps/claude-sdk-cli/test/renderStatus.spec.ts index e521d74..9d685e5 100644 --- a/apps/claude-sdk-cli/test/renderStatus.spec.ts +++ b/apps/claude-sdk-cli/test/renderStatus.spec.ts @@ -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(); diff --git a/apps/claude-sdk-cli/test/renderToolApproval.spec.ts b/apps/claude-sdk-cli/test/renderToolApproval.spec.ts index e93bed0..555b619 100644 --- a/apps/claude-sdk-cli/test/renderToolApproval.spec.ts +++ b/apps/claude-sdk-cli/test/renderToolApproval.spec.ts @@ -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;