refactor(config): replace useConfig with React Context and reload mechanism#266
Merged
LeeCheneler merged 2 commits intomajor-refactorfrom Apr 5, 2026
Merged
refactor(config): replace useConfig with React Context and reload mechanism#266LeeCheneler merged 2 commits intomajor-refactorfrom
LeeCheneler merged 2 commits intomajor-refactorfrom
Conversation
Converts useConfig from a simple useMemo hook into a React Context with ConfigProvider and a reload() function. All consumers now get config from context and can trigger a re-read from disk. The renderInk test utility now wraps all trees in ConfigProvider with automatic mock config and cleanup (afterEach unmounts Ink and restores the mock filesystem). Tests pass config overrides via the second argument instead of calling mockConfig manually.
All settings screens and the model selector now call reload() after writing config to disk, keeping the React context in sync. Chat reads provider and model from useConfig() instead of receiving them as props. The renderInk test utility exposes getConfig() which reads the live context value, used to assert that reload() was called after mutations.
LeeCheneler
added a commit
that referenced
this pull request
Apr 9, 2026
* docs: add architecture guidelines and repository restructure plan Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: move old codebase to old/, bootstrap new src/ shell - 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]> * chore: remove .js extensions from imports and add guideline Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(config): add zod config schema with nullish model/provider - 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 * refactor: extract version into its own module * feat(ui): add layout, typography, and theme primitives - 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 * fix(ui): fix BlankLine double-line output and style header version info * refactor(ui): simplify BlankLine to single line with no props * feat(config): add full config schema with zod validation (#249) * 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(config): add file loading, saving, and typed updaters (#253) * 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: add ChatInput component and useTextInput hook (#255) * 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]> * feat(chat): add useChatInput hook with multi-line cursor navigation (#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]> * feat(chat): add message history browsing and input draft preservation (#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]> * feat(chat): add contextual key instructions to ChatInput and MessageHistory (#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 ChatList with user message rendering (#259) * 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 slash command infrastructure with autocomplete (#260) * 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]> * feat(input): add word delete support (Option+Backspace and Meta+d) 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]> * feat(settings): add /settings command with generic takeover mode 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]> * refactor(ui): extract NavigationMenu from Settings Reusable keyboard-navigable menu component with cursor indicator, clamped selection, and configurable color. Settings now delegates menu rendering and input handling to NavigationMenu. * feat(ui): add looping navigation to NavigationMenu * feat(settings): add ToggleList component and Permissions screen 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]> * feat(settings): add Tools screen with enable/disable toggles 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]> * feat(ui): add theme.key color, toggle list styling and options support 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]> * feat(ui): add Form component and tool options sub-screen 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]> * test(keys): add keys.space constant and replace raw space literals Replace all stdin.write(" ") calls with stdin.write(keys.space) for consistency with other key constants in the test utility. * chore(test): enforce 100% coverage thresholds in vitest config * feat(ui): improve escape-to-clear UX with yellow highlight and shorten 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". * refactor: remove unnecessary v8 ignore statements 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. * feat(settings): add Allowed Commands screen with EditableList component 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. * refactor(ui): extract shared Border component to replace duplicated helpers * refactor(input): extract cursor utilities and rename text hook Extract splitAtCursor, word boundary, and line info helpers from text.ts into shared cursor.ts module. Replace duplicated splitAtCursor in chat-input, form, and editable-list with shared import. Rename text.ts to text-input.ts to match the hook name. * refactor(input): replace useTextInput with shared processTextEdit Add multi-line support (shift+enter, up/down line nav) to processTextEdit and eliminate the useTextInput hook. All three consumers (chat-input, form, editable-list) now use processTextEdit directly, gaining word operations for free. Net reduction of ~600 lines. * fix(input): strip newlines from input in single-line mode * feat(provider): add provider client and settings screen (#262) * feat(provider): add provider client interface and types Define the ProviderClient interface (fetchModels, fetchContextWindow, streamCompletion), OpenAI-compatible message types, streaming types, default base URLs for all three provider types, and resolveApiKey utility with env var fallbacks. * feat(provider): add OpenAI-compatible client with fetchModels and MSW test setup Add createOpenAICompatibleClient factory that implements ProviderClient for all three provider types. Includes fetchModels with provider-specific endpoint selection, auth headers, and response format handling. Set up MSW for network mocking in tests. * feat(ui): add select field to Form and options support to EditableList Add SelectFormField with inline radio-style options navigated via left/right arrows. Extend EditableList with hasOptions, onOptions, and the yellow › indicator matching ToggleList's pattern. Add updateProvider config updater for modifying existing providers. * feat(settings): add providers list screen * feat(settings): add provider options form with auto-open on add * feat(settings): add connection status check after saving provider options --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(ui): add LoadingIndicator with spinner and shimmer animation * feat(model): add /model command for provider and model selection (#263) * feat(model): add /model command with placeholder takeover screen * feat(model): add provider selection screen to model selector * feat(model): add model list with fetch, selection, and config persistence Replace the model list placeholder with real model fetching via createOpenAICompatibleClient. Shows loading indicator, error, and empty states. Model selection updates activeProvider and activeModel in global config. Fix useConfig to load from disk instead of returning hardcoded defaults. * refactor(config): replace direct loadConfig calls with useConfig hook --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(chat): add streaming LLM completions with abort support (#264) * feat(provider): add SSE stream parser * feat(provider): implement streamCompletion with SSE parsing and zod validation * feat(chat): add streaming completion with SSE parser and useCompletion hook Add SSE stream parser, implement streamCompletion in the OpenAI-compatible client with zod-validated chunk parsing, and introduce useCompletion hook for managing the streaming lifecycle (idle/streaming/complete/error). Fix leaked network requests in provider and model selector tests. * feat(chat): wire streaming completions into chat UI Integrate useCompletion hook into the chat loop. User messages trigger streaming completions, with live content rendered below the message list alongside a loading indicator. Assistant messages append to history on completion. Errors render in red via a dedicated ErrorMessage type. Fix env var leakage in provider tests. * feat(chat): add stream abort with escape key and interrupted notice Escape during streaming aborts the completion, keeping partial content. When not streaming, escape falls through to the existing clear behaviour. Adds dedicated "aborted" state to the completion lifecycle and "interrupted" message type rendered as dimmed text in the chat. --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(commands): add /context command with real context window detection Add CommandContext to the registry so handlers receive runtime state. Implement /context with a progress bar showing token usage against the real context window. Fetch context window from Ollama /api/show or /v1/models metadata with zod validation. Remove /ping command. * feat(prompt): add system prompt builder with context injection (#265) * feat(prompt): add system info builder Adds getSystemInfo() which builds a one-line system context string with OS, release, architecture, shell, username, and cwd for injection into the LLM system prompt. * feat(prompt): add git context builder Adds git helper functions (isGitRepo, getGitBranch, getDefaultBranch, getGitStatusSummary, getGitLog, isGitHubRemote, isGhCliAvailable) and getGitContext() which composes them into a formatted string for the system prompt including branch, status, recent commits, and GitHub hints. * feat(prompt): add tomo.md instructions loader Adds loadInstructions() which reads global ~/tomo.md and local .tomo/tomo.md instruction files. When both exist they are joined with a separator. Empty/whitespace-only files are ignored. * feat(prompt): wire system prompt into chat completions Adds buildSystemPrompt() which composes system info, git context, and tomo.md instructions into a single string. The system message is prepended at index 0 of every completion request. The prompt is rebuilt on each send so git status stays current. Includes TODO stubs for tool guidance, agent orchestration, and skills sections to be added after those features are implemented. * refactor(prompt): rename mockGitCommands to mockCommands The helper mocks any execSync call by substring matching, not just git commands. The name now reflects its actual scope. * refactor(provider): move buildProviderMessages to provider module Extracts message building from chat.tsx into provider/messages.ts where it belongs alongside other provider concerns. This keeps chat focused on UI state and prepares for the agentic tool loop which will add complexity to message construction. --------- Co-authored-by: Lee Cheneler <[email protected]> * refactor(config): replace useConfig with React Context and reload mechanism (#266) * refactor(config): replace useConfig with React Context Converts useConfig from a simple useMemo hook into a React Context with ConfigProvider and a reload() function. All consumers now get config from context and can trigger a re-read from disk. The renderInk test utility now wraps all trees in ConfigProvider with automatic mock config and cleanup (afterEach unmounts Ink and restores the mock filesystem). Tests pass config overrides via the second argument instead of calling mockConfig manually. * feat(config): wire reload() into mutation sites and remove prop drilling All settings screens and the model selector now call reload() after writing config to disk, keeping the React context in sync. Chat reads provider and model from useConfig() instead of receiving them as props. The renderInk test utility exposes getConfig() which reads the live context value, used to assert that reload() was called after mutations. --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(markdown): add terminal markdown rendering and ANSI stripping (#267) * feat(utils): add stripAnsi and strip ANSI from provider messages Adds a stripAnsi utility for removing ANSI escape codes from strings. Assistant messages are now stripped before being sent to the provider to prevent rendered markdown formatting from wasting context tokens. * feat(markdown): add terminal markdown rendering for chat messages Adds renderMarkdown() and completePartialMarkdown() for converting markdown to ANSI-formatted terminal output using marked, chalk, and cli-highlight. Supports headings, inline styles, code blocks with syntax highlighting, tables with box drawing, blockquotes, and lists. Assistant messages in the static list render through renderMarkdown. Streaming content uses completePartialMarkdown to close unclosed code fences before rendering, so partial responses display correctly. --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(session): add session persistence with /new and /session commands (#268) * feat(session): write chat messages to JSONL session files Each chat session gets a unique JSONL file at ~/.tomo/sessions/<datetime>-<uuid>.jsonl. Every message (user, assistant, command, error, interrupted) is appended as a JSON line as it occurs. Also fixes pre-existing coverage gaps in config hook and test utils. * feat(commands): add /new command to start a fresh session Adds resetSession to CommandContext so commands can reset the conversation. The session path swap is deferred until after the command result is appended, keeping the result visible in Static and written to the old session file. A sessionStartIndex ref prevents old messages from being sent to the LLM after a reset. * refactor(ui): rename NavigationMenu to SelectList * feat(commands): add /session command with terminal reset on switch Adds /session to browse and load saved sessions, and loadSession to CommandContext so takeover commands can replace the conversation. TakeoverRender now receives CommandContext as a second argument. The app header moves into ChatList's Static so both header and messages share a single key. On session switch the terminal clears via ANSI escape and the Static remounts, giving a clean slate. --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(tools): add tool system with read_file, agentic loop, and permission UI (#269) * feat(tools): add tool system architecture and read_file tool Introduces the tool type system (Tool, ToolContext, ToolResult), a tool registry that produces OpenAI-compatible definitions, and a permission checker for cwd/global file access. The read_file tool is the first implementation — it reads files with numbered lines, supports line ranges, truncates at 500 lines, and gates on file permissions with interactive confirmation fallback. * chore: remove unused Permissions import * feat(tools): wire agentic completion loop with tool execution Adds the send → tool calls → execute → send results → repeat loop to useChat. Tool calls and results are tracked as ChatMessage types so they persist in conversation context across turns. Fixes SSE delta parsing to accept content: null (sent by providers alongside tool calls). Renders tool calls and results in the chat list. Tool execution errors are caught and sent as tool results rather than silently killing the loop. * style(ui): improve tool call and result rendering in chat list * feat(tools): add interactive approval UI and tool denial guidance Reworks the confirm prompt into a bordered panel with a select list, y/n keyboard shortcuts, and escape-to-deny. Shows "Awaiting approval" below the tool call while waiting for user input. Adds system prompt guidance telling the LLM to respect tool denials and not retry them. --------- Co-authored-by: Lee Cheneler <[email protected]> * feat(tools): render tool errors and denials in red Add status field to ToolResultMessage and use it in the chat list to visually distinguish ok/error/denied tool results. Errors and denials render in theme.error (red) instead of dimColor. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(tools): add diff rendering, write_file tool, and tool-controlled call summaries - Add format field ("plain" | "diff") to ToolResult, carried through to ToolResultMessage for colored diff rendering (+green, -red, @@cyan) - Extract DiffView to shared ui component used by chat list and confirm UI - Add write_file tool with permission gating and diff preview on confirmation - Generate diff before writing so users see changes before approving - Replace generic formatArgs with per-tool formatCall method so each tool controls its own display summary (read_file/write_file show just the path) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(tools): add edit_file tool and fill test coverage gaps - Add edit_file tool with exact string replacement, uniqueness check, diff preview on confirmation, and write permission gating - Add formatCall tests for read_file, write_file, and edit_file - Add chat.tsx test for diff rendering in confirm prompt Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(tools): extract tool execution loop from Chat into standalone function Move tool call execution logic out of useChat into executeToolCalls(), a pure async function that takes tool calls and returns display messages. This allows unit testing tool execution without rendering Chat with MSW, and removes ~200 lines of integration tests now covered by unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(tools): add glob tool for file pattern matching Uses git ls-files in git repos for .gitignore support, falls back to fs.globSync outside git or when gitignore is disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(tools): add grep tool for file content search Uses git grep in git repos for .gitignore support, falls back to regular grep outside git or when gitignore is disabled. Supports include filters, single file search, and extended regex (-E). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(utils): extract getErrorMessage utility and fix coverage gaps Add shared getErrorMessage() for extracting messages from unknown caught values. Replace inline instanceof checks in glob, grep, and execute-tool-calls. Add missing branch coverage tests for custom path and include-with-slash. Simplify tool defs passthrough in chat loop. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(tools): add ask tool with interactive prompt UI - Add ask tool with option selection and free-text input modes - Add AskPrompt component using SelectList, with useAskPrompt hook - Escape cancels from both modes, returning error status (red) - Add context.ask() to ToolContext for interactive user questions - Extract shared mockToolContext test utility, replacing 7 duplicates Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(test): add default MSW handler for Ollama context window requests setupMsw() now accepts default handlers that persist across test resets. Add ollamaShowHandler to all chat test suites to silence unhandled request errors from fetchContextWindow on Chat mount. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(security): replace execSync with execFileSync to prevent shell injection JSON.stringify does not escape shell metacharacters like $() and backticks inside double-quoted strings. Since tool inputs come from the LLM, this was exploitable via prompt injection. execFileSync bypasses the shell entirely, eliminating the vulnerability by construction. * fix(chat): prevent duplicate interrupted messages on config reload The completion effect re-fired when config changes caused completion.send to get a new reference, re-processing the stale "aborted" state. A handledStateRef guard now skips already-processed terminal states. Adds regression tests for abort + config reload and abort + new message flows. * feat(chat): nudge LLM on empty responses with user-visible status When the LLM returns no content and no tool calls, re-send with a nudge prompt (up to 3 retries). Each retry appends a dimmed info message so the user knows what's happening. If all retries are exhausted, a terminal info message tells the user to try again. * fix(chat): show full diff output and improve truncation message Diff output was truncated to 12 lines, hiding changes from the user before approval. Diffs are now shown in full since they render in Ink's Static area. Plain tool output truncation message now shows the hidden line count (e.g. "…[5 more lines]"). * feat(context): add token-aware context window truncation Uses js-tiktoken (cl100k_base) to count message tokens and drops oldest non-system messages when the conversation exceeds the context window budget. Truncation runs inside useCompletion.send() so all send paths are covered. Reserves 5% headroom for tokenizer variance and 4096 tokens for the model response. * feat(tools): add run_command tool with streaming output Shell command execution tool with allowed-commands auto-approval, compound command detection, configurable timeout, and live output streaming via onProgress. Extends ToolContext with allowedCommands and onProgress callback. Adds LiveToolOutput component showing the last N lines during execution. * fix(chat): stabilize provider reference and guard terminal clears Memoize the provider lookup to prevent .find() from creating a new object each render, which cascaded into useCompletion recreating its send callback and re-triggering the completion effect. Guard terminal clear escape sequences with isTTY so they don't fire during tests. * feat(tools): add web_search tool with Tavily API integration Tavily-backed web search tool returning up to 5 results with AI answer summary. API key resolved from config with TAVILY_API_KEY env var fallback. Adds webSearchApiKey to ToolContext and wires it through from config in chat.tsx. * feat(prompt): include current date and time in system prompt * feat(skills): add skill types and loader Introduce SkillDefinition type and loader that reads SKILL.md files from global (~/.tomo/skills/) and local (./.tomo/skills/) directories, parsing YAML frontmatter for name and description. Fix mock-fs listDir to return subdirectory names matching real readdirSync behavior. * feat(skills): add skill registry and input detection SkillRegistry stores skills keyed by name+source, with local priority on lookup. isSkill() detects // prefix for skill invocations. * fix(config): stabilize reload callback to prevent infinite re-render Wrap ConfigProvider's reload function in useCallback so consumers depending on it in useEffect don't trigger an infinite update loop. * feat(chat): extend autocomplete to support skill invocation via // Add skill autocomplete mode triggered by // prefix, alongside the existing / command autocomplete. AutocompleteItem gains key and tag fields to support duplicate names across sources and (local) labels. * refactor(skill): extract isSkill detection to utils and enhance autocomplete Moved isSkill function from src/skills/is-skill.ts to src/skills/utils.ts with ParsedSkill interface and parseSkillInput helper. Enhanced autocomplete to support skill invocation via // prefix. Updated chat components to integrate skill detection and tool execution. Refactored skill registration and loading. * feat(skill-sets): add git operations for skill set sources Clone, pull, and remove skill set repos at ~/.tomo/skill-sets/ using SHA256-based slugs for stable directory names. Shallow clone for minimal disk usage. * feat(skill-sets): add discovery and loading for skill sets Discover skill sets via tomo-skills.json manifests in cloned repos. Load skills from enabled sets namespaced as setName:skillName. * feat(skill-sets): add config updaters for skill set sources Add addSkillSetSource, removeSkillSetSource, and updateSkillSetEnabledSets for managing skill set sources and their enabled sets in global config. * feat(skill-sets): add settings UI for managing skill set sources Source list with add/remove/edit URLs, options sub-screen with toggle list of discovered sets and update (pull) action. Clone on add, remove clone on delete. Wire into settings routing. * feat(skill-sets): wire enabled skill set skills into app registry Load skills from enabled skill sets at startup, registered before global/local skills so they have lowest priority. Skills appear in // autocomplete as setName:skillName. * fix(skill-sets): clone before persisting config and harden startup 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]> * feat(tools): add skill tool for model-invoked skill lookups 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]> * fix(chat): preserve usage data across stream interruptions 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]> * feat(images): add clipboard reading, message type, and provider conversion 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]> * feat(images): add image paste, detection, and nav to chat input 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]> * feat(images): wire image support end-to-end 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]> * fix(images): clear images on escape and highlight during confirmation 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]> * refactor(ui): trim instruction bars to contextual hints only 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]> * feat(ui): add prompt queue, structured confirm context, and system prompt 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. * refactor(tools): parallel execution with paired output and tail display 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. * feat(agents): add sub-agent tool with completion loop and streaming output 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. * feat(agents): stream tool invocations and keep shimmer on live headers * feat(tools): hoist completed tools to static individually * feat(mcp): add settings screen for managing MCP server connections 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. * feat(mcp): add stdio runtime with lazy background connect 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. * style(mcp): drop unused biome-ignore suppressions 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. * feat(mcp): collapse command and args into a single command field 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. * refactor: remove all non-null assertions and clean up unused imports 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. * feat(ui): scrollable window for SelectList with maxVisible prop 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. * feat(mcp): add streamable-HTTP transport 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. * feat(chat): open conversation in system pager on tab 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. * feat(commands): add /help command listing commands and tips 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. * docs: rewrite README and CONTRIBUTING for new src layout 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. * chore: remove old reference impl, migration plan, and CLAUDE.md 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). * docs: clean up CLAUDE.md and sync to .tomo/tomo.md 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). * fix(mcp): add .js extension to SDK subpath imports 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. * fix(skill-sets): make git tests resilient to default branch config 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. --------- Co-authored-by: Lee Cheneler <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
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
Replaces the one-shot
useMemoconfig hook with a React Context that supportsreload(), fixing stale config after/modelor/settingschanges. All mutation sites now callreload()after disk writes, and Chat reads provider/model from context instead of receiving them as props.GitHub Issue
N/A
What Changed
Config hook → Context provider:
useConfig()now returns{ config, reload }from aConfigProviderthat wraps the app tree. Callingreload()re-reads config from disk and triggers re-renders of all consumers.Mutation sites wired: Every settings screen (providers, permissions, tools, allowed commands) and the model selector call
reload()after writing config to disk — 11 call sites total, each with a test assertion proving the context was updated.Prop drilling removed: Chat reads
activeProvider/activeModelfrom context directly instead of receiving them as props from App.Test infrastructure improved:
renderInknow wraps all trees inConfigProviderwith automatic mock config and cleanup viaafterEach. Tests pass config overrides via a second argument instead of callingmockConfigmanually. A newgetConfig()helper on the return value reads the live context for assertions.Notes for Reviewers
AppHeaderis inside Ink's<Static>so it won't visually update after config changes — this is unchanged from before and is by design (static content scrolls off screen).