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
13 changes: 7 additions & 6 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Every session has three phases: start, work, end.

<!-- BEGIN:REPO:current-state -->
## Current State
Branch: `feature/status-state` — PR #194 open (step 5a), auto-merge set.
Branch: `feature/conversation-state` — PR #196 open (step 5b), auto-merge set.

Active development is in **`apps/claude-sdk-cli/`** — a TUI terminal app built on `@shellicar/claude-sdk`.

Expand All @@ -82,12 +82,13 @@ Follows a State / Renderer / ScreenCoordinator (MVVM) pattern. Each substep ship
- **3c** `renderEditor(state, cols): string[]` pure renderer extracted — PR #191
- **4a** `AgentMessageHandler` stateless cases extracted from `runAgent.ts` — PR #192
- **4b** `AgentMessageHandler` stateful cases moved in (`message_usage`, `tool_approval_request`, `tool_error`) — PR #193
- **5a** `StatusState` + `renderStatus(state, cols): string` extracted — PR #194 (pending merge)
- **5a** `StatusState` + `renderStatus(state, cols): string` extracted — PR #194
- **5b** `ConversationState` + `renderConversation` extracted — PR #196 (pending merge)

**Next: step 5b** — extract `ConversationState` + `renderConversation` from `AppLayout`
- Move sealed blocks, active block, flush count, `transitionBlock`, `appendStreaming`, `completeStreaming`, `appendToLastSealed` to `ConversationState`
- Move render logic to `renderConversation(state, cols, availableRows): string[]`
- Largest extraction so far — flush-to-scroll and block rendering are the complex parts
**Next: step 5c** — extract `ToolApprovalState` + `renderToolApproval` from `AppLayout`
- Move `#pendingTools`, `#selectedTool`, `#toolExpanded`, `#pendingApprovals` to `ToolApprovalState`
- Move `#buildApprovalRow`, `#buildExpandedRows` logic to `renderToolApproval(state, cols): string[]`
- The async approval promise queue must move together with the state
<!-- END:REPO:current-state -->

<!-- BEGIN:REPO:vision -->
Expand Down
58 changes: 58 additions & 0 deletions .claude/sessions/2026-04-07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Session 2026-04-07

## What was done

### Step 5b — Extract `ConversationState` + `renderConversation` from `AppLayout` (PR #196)

Started from main with PRs #194 (5a StatusState) and #195 (perf test fix) already merged.

**New `ConversationState.ts`** — pure state, no I/O:
- Holds `#sealedBlocks: Block[]`, `#flushedCount: number`, `#activeBlock: Block | null`
- `addBlocks(blocks)` — for startup banner and history replay
- `transitionBlock(type)` — seals non-empty active if type differs, returns `{noop, from, sealed}` so caller can log without re-reading private state
- `appendToActive(text)` — no-op when no active block
- `completeActive()` — seals non-empty active, clears it
- `appendToLastSealed(type, text)` — checks active first, then sealed in reverse; returns `'active'`, a sealed index, or `'miss'` so caller can log the exact target
- `advanceFlushedCount(to)` — called by AppLayout after flushing blocks to scroll
- `Block` and `BlockType` types exported from here (removed from AppLayout)

**New `renderConversation.ts`** — all block rendering:
- All constants and helpers move here: `FILL`, `BLOCK_PLAIN`, `BLOCK_EMOJI`, `CONTENT_INDENT`, `CODE_FENCE_RE`, `renderBlockContent`
- `buildDivider(label, cols)` exported — AppLayout uses it for the prompt divider and the content/status separator (`buildDivider(null, cols)` replaces the old `DIM + FILL.repeat(cols) + RESET` inline)
- `renderConversation(state, cols)` — sealed blocks + active block for the alt-buffer viewport
- `renderBlocksToString(allBlocks, startIndex, cols)` — for the flush-to-scroll path; takes the full array so continuation checks work correctly for already-flushed blocks

**`AppLayout` after (684 → 619 lines, -65):**
- `#sealedBlocks`, `#flushedCount`, `#activeBlock` replaced by `#conversationState = new ConversationState()`
- `BlockType`/`Block` type definitions removed (now imported from ConversationState.ts)
- `highlight` import removed (moved to renderConversation.ts)
- `FILL` constant removed; `BLOCK_PLAIN`, `BLOCK_EMOJI`, `CONTENT_INDENT` definitions removed
- `renderBlockContent`, `buildDivider` functions removed
- All block mutation methods delegate to `#conversationState`
- `render()` replaces 50-line block loop with `renderConversation(this.#conversationState, cols)`
- `#flushToScroll()` replaces 25-line loop with `renderBlocksToString(sealedBlocks, flushedCount, cols)`
- The `CONTENT_INDENT` constant is kept locally in AppLayout (still used by `#buildExpandedRows` and `#buildPreviewRows` until steps 5c/5d extract those)

**Tests: 41 new (231 total, up from 190):**
- `ConversationState.spec.ts` (27): initial state, addBlocks, transitionBlock sequences (noop/seal/discard/from tracking), appendToActive, completeActive, appendToLastSealed (active/sealed-index/miss/most-recent-selection), advanceFlushedCount
- `renderConversation.spec.ts` (14): empty state, single block structure (divider/blank/content/trailing-blank), continuation suppression (no header between same-type blocks, no gap), active block rendering (new divider when different type, suppressed when continuation), buildDivider (null/label/fill)

## Decisions

**`transitionBlock` return value:** Returns `{noop, from, sealed}` rather than void. The caller (AppLayout) needs this info for debug logging. Alternative was logging before calling, but that would require AppLayout to pre-read private state from ConversationState. Returning a result struct keeps the state machine opaque.

**`appendToLastSealed` return value:** Returns `'active'`, a number (sealed index), or `'miss'`. Restores the original log fidelity (`{ index: i }`) without leaking the search loop into AppLayout.

**`buildDivider(null, cols)` for the separator:** The function was already handling null → plain fill. Using it for the content/status separator removes the last direct reference to `FILL` from AppLayout and makes both divider types go through one path.

**`renderBlocksToString` takes full array:** Not a slice. The continuation check for block `i` references `allBlocks[i-1]`, which may be a block already flushed to scroll. Passing the full array ensures headers are correctly suppressed when consecutive same-type blocks span a flush boundary.

**`CONTENT_INDENT` kept locally in AppLayout:** Steps 5c and 5d will extract tool approval and command mode renderers, which are the remaining users of `CONTENT_INDENT` in AppLayout. Duplicating the 3-char constant temporarily is less disruptive than exporting it from `renderConversation.ts` (which would create an odd dependency from the coordinator on a conversation-specific renderer's internals).

## What's next

**Step 5c** — extract `ToolApprovalState` + `renderToolApproval`:
- Move `#pendingTools`, `#selectedTool`, `#toolExpanded`, `#pendingApprovals` to `ToolApprovalState`
- Move `#buildApprovalRow` + `#buildExpandedRows` logic to `renderToolApproval(state, cols): string[]`
- Risk: medium-high. The async approval flow (promise queue resolved by keyboard handler) must move together — splitting it would leave a broken intermediate state.
- Tests: async approval flow, cancel flow, keyboard navigation (left/right cycle, space expand).
Loading
Loading