refactor: rebuild src/ as feature-grouped layout#270
Merged
LeeCheneler merged 98 commits intomainfrom Apr 9, 2026
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Move existing source to old/ as reference implementation - Create new src/ with Ink shell app, config hook, and app header - Add CLAUDE.md with coding guidelines, comment rules, and testing rules - Add coverage/ to .gitignore - Add node types to tsconfig Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add config schema and inferred Config type - Update useConfig hook to return typed Config with null defaults - Update AppHeader to show fallback when model/provider missing - Add tests for schema, hook, and header fallback states - Restrict vitest to src/ only via vitest.config.ts
- Add Indent and BlankLine layout components - Add Heading and Hint typography components - Add theme color palette for consistent theming - Refactor AppHeader to use primitives and co-located hook
* feat(config): add provider schema and types * feat(config): add permissions schema with cwd/global file access * feat(config): add tools schema and extract sub-schemas for isolation * feat(config): add allowedCommands schema * feat(config): add agents schema with maxTimeoutSeconds * feat(config): add MCP connections schema with stdio and HTTP transports * feat(config): add skillSets schema with sources and nested enabledSets --------- Co-authored-by: Lee Cheneler <[email protected]>
* feat: add fs utils, test utils for mock filesystem, and config file paths * feat(config): add loadConfig with YAML loading, merging, and validation * feat(config): add saveGlobalConfig and saveLocalConfig for whole file writes * feat(config): add updateGlobalConfig and updateLocalConfig read-modify-write primitives * feat(config): add typed updaters with zod-validated reads, zero casts * feat(config): add typed updaters with zod-validated reads, zero casts --------- Co-authored-by: Lee Cheneler <[email protected]>
* feat(ui): add ChatInput component with bordered input and status ribbon Refs #254 * feat(ui): add keyboard input handling to ChatInput Refs #254 * feat(ui): add cursor tracking with visible block cursor and left/right movement Refs #254 * feat(ui): add option+arrow and ESC+b/f word jump for cursor Refs #254 * feat(ui): add shift+enter newline support in ChatInput Refs #254 * feat(ui): add up/down arrow for cursor start/end navigation Refs #254 * refactor(ui): extract useTextInput hook into shared input/text module Refs #254 * refactor(input): extract useTextInput to src/input and fix word boundary detection Moves the shared text input hook out of src/ui/input into src/input. Word jump now uses \w to detect word boundaries, stopping at punctuation and newlines instead of only spaces. Refs #254 * feat(input): add lineMode option to useTextInput Refs #254 * feat(input): add onUp/onDown boundary callbacks to useTextInput Refs #254 --------- Co-authored-by: Lee Cheneler <[email protected]>
…256) * feat(chat): add useChatInput hook with basic state and submit Refs #254 * refactor(chat): co-locate ChatInput component and hook, add history up navigation Merges ChatInput component and useChatInput hook into a single co-located file at src/chat/chat-input.tsx. Removes status bar, exposes setCursor from useTextInput, and adds history storage with up arrow navigation. Refs #254 * refactor(chat): strip message history from ChatInput History will be a separate MessageHistory component with its own rendering and navigation, not embedded in the input hook. Refs #254 * feat(input): add multi-line cursor navigation and fix newline cursor rendering Up/down arrows now move between lines at the same column, clamping to line length on shorter lines. First line up goes to start, last line down goes to end, with boundary callbacks at the edges. Extracts splitAtCursor for testable cursor rendering on newlines and end-of-value. Refs #254 --------- Co-authored-by: Lee Cheneler <[email protected]>
…#257) * feat(chat): add onUp and initialValue props to ChatInput * feat(chat): add useHistory hook for message history storage * feat(chat): add double-escape to clear input with confirmation hint * feat(chat): add MessageHistory component for browsing sent messages * feat(chat): add Chat router with history mode and magenta theme * fix(chat): remove unused vi import from chat test * refactor(test): add renderInk wrapper and keys constants for readable tests * feat(chat): preserve draft input when switching to history mode --------- Co-authored-by: Lee Cheneler <[email protected]>
…istory (#258) * feat(ui): add InstructionBar component for key-action hints Renders a row of yellow key + dim description pairs separated by · dividers. Will be used by ChatInput and MessageHistory to show contextual instructions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(chat): add KeyInstructions to ChatInput with contextual hints Replace the hardcoded escape hint with a KeyInstructions bar that shows relevant key actions based on input state: enter/submit and escape/clear when content is present, escape/confirm when pending, and up/history when message history is available. Rename InstructionBar to KeyInstructions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(chat): add contextual KeyInstructions to MessageHistory and refine ChatInput hints MessageHistory shows up/down/esc/enter instructions that adapt to position: esc/down combines on the last entry, up/down only appear when navigation is possible. ChatInput now shows "up history" only at cursor position 0 and updates immediately after first message send. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(chat): make useHistory state-driven, remove hasHistory workaround Switch useHistory from ref-based HistoryApi class to useState so push triggers re-renders. This lets Chat derive hasHistory directly from history.entries.length instead of tracking it as separate state. Export flushInkFrames from test utils for tests needing manual flush. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(chat): move instruction derivation into hooks Instruction building logic belongs in hooks, not components. Move it into useChatInput and useMessageHistory with a declarative filter pattern. Components now receive instructions as data and render only. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Lee Cheneler <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(chat): add ChatMessage type with user message variant * feat(chat): add ChatList component and wire user messages into Chat Add ChatList using Ink's Static to render submitted messages above the input. User messages display with a cyan ❯ indicator in a flex layout so multi-line content aligns correctly. Wire into Chat so submitted messages persist in the list. Also align ChatInput prompt the same way and add spacing below the app header. --------- Co-authored-by: Lee Cheneler <[email protected]>
) * feat(commands): add command registry for slash commands * feat(chat): add CommandMessage type and chat list rendering * feat(commands): add command parsing, execution, and /ping command Registry invoke handles parsing the command name, executing the async handler, and returning a CommandMessage. Unknown commands are styled with the theme error color. Chat delegates detection to isCommand and execution to the registry. Ping command wired as first built-in. * feat(chat): add inline autocomplete for slash commands AutocompleteList renders filtered suggestions below ChatInput while typing a / command. Up/down navigate the list (captureUpDown on useTextInput), enter fills the selected command with a trailing space to dismiss. Space in the input hides autocomplete. Instructions swap between navigate/select and normal submit/clear/history modes. * fix(chat): use sliding window for autocomplete scrolling Selection now moves through all filtered items, not just the first 5. The visible window slides lazily when the cursor hits the top or bottom edge. Extracted getWindowStart and removed the slice from filterAutocompleteItems so windowing is handled by the caller. * refactor(chat): extract autocomplete state into useAutocompleteNavigation Move selectedIndex, windowStart, filtering, and navigation logic out of useChatInput into a dedicated hook in the autocomplete module. ChatInput now delegates to moveUp/moveDown/select/reset instead of managing autocomplete internals directly. * chore: remove unused AutocompleteNavigation import * refactor(chat): make key instructions static in both chat modes Replace dynamic, state-dependent instructions with static sets. ChatInput always shows enter/submit, escape/clear, up/history, and /command. MessageHistory always shows up/down/scroll, esc/return to draft, and enter/replace draft. Only escape confirm and the autocomplete mode swap remain dynamic. Removes significant conditional logic. * chore: remove temporary test command registrations --------- Co-authored-by: Lee Cheneler <[email protected]>
Reuses existing word boundary functions to delete whole words, mirroring the Option+Left/Right word jump behaviour. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Introduces a takeover mechanism for commands that need to take over the chat screen. The registry returns a discriminated InvokeResult (inline or takeover) and Chat renders the takeover component without knowing what it is. The /settings command uses this to render a navigation menu with placeholder sub-screens. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Reusable keyboard-navigable menu component with cursor indicator, clamped selection, and configurable color. Settings now delegates menu rendering and input handling to NavigationMenu.
Add reusable ToggleList UI primitive with looping cursor navigation and space/enter toggling. Implement Permissions settings screen that loads config, renders four file-access permission toggles, and persists changes to local config immediately on toggle. Wire Permissions into the settings router. Settings now always returns "Settings updated" via onDone. Fix takeover command name bug where /settings displayed as "/" in message history by storing the command name in takeover mode state. Add dimmed cursor prefix to command messages matching user message style. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Implement ToolsScreen that displays all 10 first-party tools as a toggle list, persisting changes to local config. Extra fields like webSearch.apiKey are preserved when toggling enabled state. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add theme.key color and use it in KeyInstructions and ToggleList. Style toggle indicators with dim brackets and green checkmarks. Add hasOptions/onOptions support to ToggleList — items with options show a yellow › indicator and fire onOptions on Tab. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add generic Form component with toggle and text field types. Up/down navigates fields, space toggles, typing edits text inline, enter submits, escape cancels. Replace ToolOptionsScreen with Form-based options sub-screen in ToolsScreen. Web Search options form includes Enabled toggle and API Key text field. Make space the only toggle key across ToggleList and Form for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replace all stdin.write(" ") calls with stdin.write(keys.space) for
consistency with other key constants in the test utility.
…n key labels Replace dynamic escape/confirm instruction swap with static "esc clear". First escape highlights input text in yellow inverse to signal pending clear. Standardise all key instruction labels from "escape" to "esc".
Simplify buildFormFields and applyFormValues to remove dead-code branching — only webSearch has options currently. Replace redundant type guards with simpler control flow. Reduces v8 ignore count from 12 to 5, keeping only those required for TypeScript type narrowing.
Add a reusable EditableList component for inline editing of string lists — items are editable when focused, enter saves/adds/removes, and navigating away discards unsaved edits. Wire it up as the Allowed Commands settings screen with config persistence and :* prefix syntax guidance.
Clone skill set sources before saving to config so failed clones don't leave orphaned entries. Wrap skill set discovery in try/catch at startup to prevent a broken source from crashing the app. Use bare `git pull` instead of hardcoding `origin main` so repos with non-main default branches work correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Factory-based tool that closes over the skill registry so the model can invoke skills via tool calls. Bakes available skill names into the tool description and returns helpful errors for unknown skills. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Stop clearing usage on new send() calls so /context still reports the last known token count when a stream is aborted before the final SSE chunk arrives. Only update usage when the stream actually delivers new data. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…rsion macOS clipboard access via osascript with two strategies: file reference (Finder copies) and raw image data (screenshots). Add optional images field to UserMessage and convert to ContentPart[] when building provider messages. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Ctrl+V reads clipboard images via osascript. Image file paths in typed text are auto-detected and extracted. Down arrow at end of input enters image nav mode for browsing and removing attachments. onMessage now passes images alongside the text. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Images flow from chat input through to provider messages and session persistence. Chat list shows image badges on user messages. History recall preserves attached images so users can edit text and resubmit without re-pasting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Double-escape now clears attached images along with text. Escape pending triggers when only images are attached. Image tags highlight in yellow inverse during escape confirmation to match the text style. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Drop obvious instructions (up/down navigate, enter submit, left/right select) across all screens. Chat input esc is hidden by default and only appears as "interrupt" when streaming or "clear" during esc-pending. Editable list enter is contextual: "save" normally, "remove" when item text is cleared. Dimmed removal hint added above editable lists. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ompt enforcement Replace single-slot confirm/ask with a FIFO prompt queue to support concurrent tool executions. Extend ConfirmOptions with label/detail fields rendered inside the bordered approval prompt. All tools now pass structured context (e.g. "Run command?" + the command string). Overhaul system prompt with enforced CRITICAL directives: parallel tool calls, read-before-write, dedicated tool usage, and compound command splitting — each with WRONG/CORRECT examples that dramatically improved model tool use compliance.
Execute tool calls in parallel via Promise.all with per-tool scoped onProgress for multi-slot live output. Return interleaved [call, result] pairs so each tool call renders next to its output in the chat list. Dynamic area shows tool call headers paired with their live streaming output during execution, then batch-hoists everything to Static when the batch completes. Provider messages merge interleaved display format back into a single assistant turn for the API. Static tool output now tails (last N lines) instead of truncating (first N lines) — test results and build errors at the end of output are more useful than opening boilerplate.
…utput Introduce the agent tool for spawning autonomous sub-agents with their own chat completion loops. Sub-agents stream content to the parent's live output slot via onProgress/onContent, showing real-time progress in the dynamic area with yellow-hued shimmer loading indicators. Includes concurrency semaphore, configurable timeout, depth-based tool scoping, and confirm wrapping that injects the agent name into approval prompts. Sub-agent system prompt enforces parallel tool calls and provides exploration strategy guidance.
Adds the MCP Servers settings screen, replacing the placeholder. Backed by a new KvEditor primitive and a kv field type on Form for editing env vars and HTTP headers. Schema simplified — kvEntrySchema removed since sensitive masking offered no protection over plaintext config. The runtime client/manager comes in a follow-up.
Connects to configured stdio MCP servers in the background when the chat mounts, registers discovered tools into the shared registry as each server comes up, and surfaces connection failures as inline chat error messages. Sub-agents auto-inherit any tool namespaced mcp__* regardless of the agents allowlist. Uses the official SDK for the transport layer and a dependency-injectable client factory on the manager so lifecycle and error paths can be tested without real subprocesses.
The ignore comments referenced a rule name that does not exist, so biome flagged them as ineffective. Replaced with plain comments explaining why the tests throw non-Error values.
The separate Command and Args form fields invited the common footgun of typing the whole command line into Command and leaving Args empty — which then spawns a binary literally named "node server.mjs" and fails with ENOENT. Now the form has one Command field that holds the full line and splits on whitespace at save time. joinCommand/splitCommand are exported and unit-tested so the parsing edge cases (empty, whitespace-only, runs of whitespace) have direct coverage.
Five pre-existing biome warnings are gone: - chat.tsx: unused Text import removed - chat.tsx: providerRef/modelRef refs dropped entirely; the effect now closes over provider and model directly (with deps updated to match) and handles the "provider cleared mid-stream" race by appending an error message - chat.tsx: liveToolOutputs.get(...)! replaced with a local const plus an undefined check - messages.ts: tool_calls!.push restructured — instead of reassigning an optional field on the message, we track the tool_calls array in a narrowed local and push into it directly, which also mutates the reference already attached to the message in the result list The renderInk test helper now exposes reloadConfig(), which triggers a config reload from the (mutated) mock filesystem. A new chat test uses it to exercise the provider-cleared-mid-stream branch by holding the completion stream open, rewriting the config file, reloading, then releasing the stream.
Adds an optional maxVisible prop to SelectList. When the list exceeds it, only a window of that many items renders, with "↑ N more" and "↓ N more" indicators above and below as the cursor moves. Wrap-around navigation jumps the window so the cursor stays visible. /session now uses this with maxVisible=5 so long session histories stop pushing the chat off the screen.
Extends createMcpClient to route on transport type. HTTP connections use the SDK's StreamableHTTPClientTransport with connection.headers mapped to requestInit.headers. Extracted a shared wrapTransport helper so the stdio and http factories share a single protocol-layer implementation — all the Client.listTools / callTool / disconnect logic lives in one place. No changes needed to the manager, adapter, useMcp hook, settings screen, or sub-agent integration — they only know the generic McpClient interface, so the new transport plugs in transparently. Tests spawn the real mock-mcps/http.mjs via beforeAll/afterAll, waiting for the "listening" stdout line before running. Covers connect, listTools, callTool, argument passing, custom headers, clean disconnect, and a full manager → adapter → Tool.execute end-to-end round-trip through get_weather.
Pressing Tab from the chat input renders the current conversation to ANSI text and pipes it to the system pager (less by default). PAGER env var is respected against an allowlist of less, more, most, bat, cat. Tab is gated to no-op when no messages exist or when the autocomplete list is open.
Auto-generates a help screen from the command registry with four sections: Commands, Skills, Images, Tips. Uses a factory pattern so the command can close over its own registry, and is registered last so it appears in its own listing.
Both files are rewritten from scratch to match the current code. README fixes the slash command list, config schema, MCP and skill set shapes, and adds the Tab pager and /help command. CONTRIBUTING documents the real feature-grouped structure, the actual coding rules, and the verified CI / lefthook setup.
The major-refactor is complete. Drops the old/ reference implementation, repository-restructure.md (the migration plan), and CLAUDE.md (rules now live in CONTRIBUTING.md as the single source of truth for human and assistant contributors).
Drops the repository-restructure.md and old/ references (refactor is done), the non-existent src/hooks/ rule, and the integration tests subsection (src/__tests__/integration/ does not exist).
esbuild's resolver requires an explicit .js extension for the StdioClientTransport and StreamableHTTPClientTransport imports because the SDK's package.json exports map uses a literal "./*" → "./dist/esm/*" rule. tsx and tsc with bundler resolution both accept the extensionless path, so this only failed under `pnpm build`. Clarifies the no-.js-extensions rule in the docs to scope it to internal/relative imports.
Both cloneSource and pullSource setup blocks now pass --initial-branch=main to git init explicitly. Previously the bare repo's HEAD pointed to whatever git's init.defaultBranch was set to (master on the CI runner), so clones came back with a dangling HEAD and no checkout. The build failure had been masking this since the tests never got a chance to run on CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rebuilds the entire
src/tree as a feature-grouped codebase, replacing the previous type-grouped layout. 96 commits, 318 files changed (+31,684 / -25,490). The refactor was developed on a long-running branch with the old code preserved underold/until the new code reached parity, thenold/was deleted.GitHub Issue
N/A
What Changed
Architecture. Code is now grouped by feature rather than type. Each feature owns its components, hooks, and logic in a single directory (
chat/,settings/,session/,model/,mcp/,skills/,skill-sets/,tools/, etc.). Shared stateless primitives live inui/. Feature-specific hooks are co-located in the same file as their component rather than living in a separatehooks/directory.Component patterns. Business logic moved into hooks; components are pure rendering. Menus are self-sufficient — they own their state and step navigation, parents receive a result. Indentation is handled via
<Indent>/<Box paddingLeft>instead of hardcoded space strings.Tools. All built-in tools (read, write, edit, glob, grep, run command, ask, skill, agent, web search) ported with full coverage. New tool execution flow with confirm prompts, diff rendering, and per-tool live progress in the chat list.
MCP. New MCP client with stdio and streamable-HTTP transports, lazy background connection, and a
/settings → MCP ServersUI. Config schema moved frommcpServerstomcp.connectionsand supports\${ENV_VAR}interpolation.Settings UI. New
/settingsmenu replaces the old multi-command setup flow. Manages providers, permissions, allowed commands, tools, MCP servers, and skill sets — everything previously requiring~/.tomo/config.yamledits.Config schema. Rewritten with zod. Permissions split into
cwdReadFile/cwdWriteFile/globalReadFile/globalWriteFile. Skill sets nested underskillSets.sources[].enabledSets. Agents config exposesmaxDepth,maxConcurrent,maxTimeoutSeconds, and a per-agent tool allowlist.New end-user features
Tests. 1399 tests, 100% coverage. Test utilities under `src/test-utils/` (`renderInk`, `keys`, MSW helpers). Tests are colocated with source.
Docs. README and CONTRIBUTING fully rewritten to match the new code. CLAUDE.md cleaned up and synced into `.tomo/tomo.md` so the same rules apply when dogfooding tomo.
Notes for Reviewers