docs: update README with full config reference, remove completed roadmap
Expands README with complete config.yaml reference, CLI options table, skills documentation, and updated command list. Removes the old roadmap (all phases complete). Updates tweaks.md with current design notes. Adds .sneakycode/ to .gitignore and includes superpowers design specs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,3 +34,6 @@ htmlcov/
|
||||
|
||||
# Worktrees
|
||||
.worktrees/
|
||||
|
||||
# SneakyCode local data
|
||||
.sneakycode/
|
||||
|
||||
122
README.md
122
README.md
@@ -26,31 +26,79 @@ pip install -e ".[dev]"
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config/config.yaml` to configure the agent. Key settings:
|
||||
Edit `config/config.yaml` to configure the agent. The full configuration reference:
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
model: "qwen3.5:latest" # Ollama model name
|
||||
endpoint: "http://localhost:11434" # Ollama endpoint
|
||||
max_retries: 3 # Retry attempts on transient errors
|
||||
retry_backoff_base: 1.0 # Exponential backoff base (seconds)
|
||||
model: "qwen3.5:latest" # Ollama model name
|
||||
endpoint: "http://localhost:11434" # Ollama endpoint
|
||||
api_path: "/v1/chat/completions" # API endpoint path
|
||||
temperature: 0.1 # Sampling temperature
|
||||
max_tokens: 4096 # Maximum tokens in LLM response
|
||||
timeout: 120 # Request timeout in seconds
|
||||
max_retries: 3 # Retry attempts on transient errors
|
||||
retry_backoff_base: 1.0 # Exponential backoff base (seconds)
|
||||
retry_backoff_max: 30.0 # Maximum backoff seconds
|
||||
|
||||
agent:
|
||||
max_iterations: 25 # Max tool-call iterations per turn
|
||||
max_conversation_tokens: 32000 # Token budget for conversation
|
||||
workspace_root: "." # Project directory for file operations
|
||||
truncation_keep_recent: 10 # Messages preserved during truncation
|
||||
truncation_threshold: 0.85 # Budget fraction that triggers truncation
|
||||
max_iterations: 25 # Max tool-call iterations per turn
|
||||
max_conversation_tokens: 32000 # Token budget for conversation
|
||||
workspace_root: "." # Project directory for file operations
|
||||
truncation_keep_recent: 10 # Messages preserved during truncation
|
||||
truncation_threshold: 0.85 # Budget fraction that triggers truncation
|
||||
|
||||
session:
|
||||
auto_save: true # Save session after each turn
|
||||
max_session_age_hours: 72 # Auto-cleanup old sessions
|
||||
offer_resume: true # Offer to resume on startup
|
||||
session_dir: ".sneakycode/sessions" # Directory for session files
|
||||
auto_save: true # Save session after each turn
|
||||
max_session_age_hours: 72 # Auto-cleanup old sessions
|
||||
offer_resume: true # Offer to resume on startup
|
||||
|
||||
permissions:
|
||||
auto_approve: [read_file, list_dir, grep_files, find_files, finish]
|
||||
prompt_user: [write_file, delete_file, run_command, str_replace, patch_apply, make_dir]
|
||||
deny: []
|
||||
|
||||
tools:
|
||||
shell:
|
||||
allowed_commands: # Commands the LLM may run
|
||||
- git
|
||||
- python
|
||||
- pip
|
||||
- pytest
|
||||
- ruff
|
||||
- ls
|
||||
- cat
|
||||
- head
|
||||
- tail
|
||||
- wc
|
||||
- diff
|
||||
- grep
|
||||
- find
|
||||
- echo
|
||||
denied_commands: # Blocked commands
|
||||
- rm -rf /
|
||||
- sudo
|
||||
- curl
|
||||
- wget
|
||||
max_output_bytes: 65536 # Max captured output size (bytes)
|
||||
filesystem:
|
||||
max_file_size_bytes: 1048576 # 1 MB — max file size for read/write
|
||||
binary_detection: true # Detect and reject binary files
|
||||
|
||||
display:
|
||||
show_tool_calls: true # Show tool call details in output
|
||||
show_token_usage: true # Show token usage stats
|
||||
stream_output: true # Stream LLM output to terminal
|
||||
|
||||
skills:
|
||||
enabled: true # Enable the skills system
|
||||
directories: # Directories to scan for skill files
|
||||
- ".sneakycode/skills"
|
||||
|
||||
debug:
|
||||
enabled: false # Enable debug logging
|
||||
log_dir: ".sneakycode/logs" # Debug log directory
|
||||
max_files: 10 # Max debug log files to retain
|
||||
```
|
||||
|
||||
Environment variable `SNEAKYCODE_CONFIG` can override the config file path.
|
||||
@@ -58,9 +106,12 @@ Environment variable `SNEAKYCODE_CONFIG` can override the config file path.
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Start the interactive REPL
|
||||
# Start the interactive TUI
|
||||
sneakycode
|
||||
|
||||
# Open a specific project directory
|
||||
sneakycode /path/to/project
|
||||
|
||||
# Or run directly
|
||||
python -m app.main
|
||||
|
||||
@@ -68,25 +119,38 @@ python -m app.main
|
||||
sneakycode --config path/to/config.yaml --verbose --log-file sneakycode.log
|
||||
```
|
||||
|
||||
### CLI Options
|
||||
|
||||
| Option | Description |
|
||||
|--------------------------|--------------------------------------------------|
|
||||
| `DIRECTORY` | Project directory to use as workspace root |
|
||||
| `--config PATH` | Path to config YAML file (default: `config/config.yaml`) |
|
||||
| `-v`, `--verbose` | Enable verbose (DEBUG) logging |
|
||||
| `--log-file PATH` | Path to log file for persistent logging |
|
||||
|
||||
### REPL Commands
|
||||
|
||||
| Command | Description |
|
||||
|------------|--------------------------------------|
|
||||
| `/quit` | Save session and exit |
|
||||
| `/history` | Show conversation history |
|
||||
| `/clear` | Clear conversation history |
|
||||
| `/save` | Manually save session |
|
||||
| `/session` | Show session info (messages, tokens) |
|
||||
| Command | Description |
|
||||
|-------------------|----------------------------------------------------|
|
||||
| `/help` | Show available commands |
|
||||
| `/quit` | Save session and exit (also `/exit`, `/bye`) |
|
||||
| `/history` | Show conversation history |
|
||||
| `/clear` | Clear conversation history |
|
||||
| `/save` | Manually save session |
|
||||
| `/session` | Show session info (messages, tokens, start time) |
|
||||
| `/models` | List available Ollama models |
|
||||
| `/models <name>` | Switch to a different model |
|
||||
| `/skills` | List available skills |
|
||||
|
||||
### Session Persistence
|
||||
|
||||
Sessions are automatically saved after each agent turn and on exit. On startup, SneakyCode offers to resume the most recent session for the current workspace.
|
||||
|
||||
Session files are stored in `.sneakycode/sessions/` within the workspace root.
|
||||
Session files are stored in `.sneakycode/sessions/` within the workspace root (configurable via `session.session_dir`).
|
||||
|
||||
## Available Tools
|
||||
|
||||
SneakyCode provides 11 tools across 5 categories. See [docs/tools.md](docs/tools.md) for the full reference.
|
||||
SneakyCode provides tools across 6 categories. See [docs/tools.md](docs/tools.md) for the full reference.
|
||||
|
||||
| Category | Tools | Permission |
|
||||
|------------|-------------------------------------------------|---------------|
|
||||
@@ -96,6 +160,17 @@ SneakyCode provides 11 tools across 5 categories. See [docs/tools.md](docs/tools
|
||||
| Edit | `str_replace`, `patch_apply` | User confirm |
|
||||
| Shell | `run_command` | User confirm |
|
||||
| Control | `finish` | Auto-approved |
|
||||
| Skills | `load_skill` | Auto-approved |
|
||||
|
||||
The `load_skill` tool is available when `skills.enabled` is `true` in the config. It allows the LLM to load skill instructions from the configured skill directories.
|
||||
|
||||
## Skills
|
||||
|
||||
SneakyCode includes a skills system that lets you provide reusable instruction sets to the LLM. Skills are markdown files placed in `.sneakycode/skills/` (or any directory listed in `skills.directories`).
|
||||
|
||||
Skills are auto-discovered on startup. The LLM can load them via the `load_skill` tool, and you can list available skills with the `/skills` command.
|
||||
|
||||
To create a skill, add a `.md` file to your skills directory with a descriptive filename (e.g., `refactoring.md`). The file content is injected into the conversation when the skill is loaded.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -121,6 +196,7 @@ app/
|
||||
├── models/ # Pydantic config and message schemas
|
||||
├── services/ # LLM client, streaming, permissions, session persistence
|
||||
├── tools/ # Tool implementations (one file per group)
|
||||
├── ui/ # Textual TUI application and widgets
|
||||
└── utils/ # Logging, display, file helpers, token counter
|
||||
config/
|
||||
└── config.yaml # Application configuration
|
||||
|
||||
144
docs/ROADMAP.md
144
docs/ROADMAP.md
@@ -1,144 +0,0 @@
|
||||
# SneakyCode Implementation Roadmap
|
||||
|
||||
A phased plan progressing from bare-bones foundation to full autonomous coding agent.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Foundation: Models, Config, and Utilities
|
||||
|
||||
Establish the data layer and shared infrastructure everything else builds on.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/models/config.py` | Pydantic v2 config model — load and validate `config/config.yaml` |
|
||||
| `app/models/message.py` | Message schema (role, content, tool_calls) |
|
||||
| `app/models/tool_call.py` | ToolCall and ToolResult schemas |
|
||||
| `app/utils/logging.py` | Centralized logger with Rich handler |
|
||||
| `app/utils/display.py` | Rich console output helpers (stub — expanded in Phase 2) |
|
||||
| `app/utils/file_helpers.py` | Safe path resolution, binary detection, size guards |
|
||||
| `app/utils/token_counter.py` | Approximate token usage tracking (character-based heuristic for v1) |
|
||||
| `app/main.py` | Entrypoint stub — arg parsing, config load, Rich console setup |
|
||||
|
||||
**Exit criteria:** `python -m app.main --help` runs, config loads and validates, models can be instantiated and serialized.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — TUI and Interactive Shell
|
||||
|
||||
Get a working interactive terminal before wiring up the LLM.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/main.py` | Rich-based interactive REPL loop — prompt for user input, display responses |
|
||||
| `app/utils/display.py` | Formatted output for agent messages, tool calls, errors, token usage |
|
||||
| `app/agent/context.py` | Session state and conversation history management |
|
||||
|
||||
**Exit criteria:** User can type messages into a styled REPL, see them echoed back with formatting, and conversation history is tracked in memory.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — LLM Integration (Ollama)
|
||||
|
||||
Connect to the local LLM and stream responses into the TUI.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/services/llm.py` | Async httpx client wrapping Ollama's OpenAI-compatible `/v1/chat/completions` endpoint |
|
||||
| `app/services/streaming.py` | SSE parsing, Rich live display, tool call extraction from accumulated stream |
|
||||
|
||||
**Integration:** Wire LLM into the REPL — user message goes to LLM, streamed response displays in real time.
|
||||
|
||||
**Exit criteria:** User can chat with the local model through the TUI with streamed output. Tool call JSON is parsed from the stream but not yet executed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Tool Framework and Core Tools
|
||||
|
||||
Build the tool abstraction and implement safe, read-only tools first.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/tools/base.py` | `BaseTool` ABC and `ToolResult` dataclass |
|
||||
| `app/tools/registry.py` | Tool registration, discovery, and JSON schema export for LLM system prompt |
|
||||
| `app/services/permissions.py` | Two-tier approval gating (auto-approve reads; prompt for writes/deletes/shell) |
|
||||
| `app/tools/filesystem.py` | `read_file`, `list_dir` |
|
||||
| `app/tools/search.py` | `grep_files`, `find_files` |
|
||||
|
||||
**Exit criteria:** Tools register themselves, schemas export correctly for inclusion in the system prompt, read-only tools execute and return `ToolResult` objects. Permissions service gates execution.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Agent Loop (ReAct)
|
||||
|
||||
The core autonomy layer — reason, act, observe, repeat.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/agent/loop.py` | ReAct cycle: send conversation to LLM, parse tool calls, execute, feed results back, repeat |
|
||||
|
||||
**Key behaviors:**
|
||||
- System prompt constructed with tool schemas from registry
|
||||
- Permissions checks before each tool execution
|
||||
- Loop termination on: plain-text response (no tool calls), explicit `finish` tool call, or `max_iterations` exceeded
|
||||
|
||||
**Exit criteria:** Agent can autonomously answer questions about the codebase by chaining `read_file`, `list_dir`, `grep_files`, and `find_files` tool calls in a multi-turn loop.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Write Tools and Shell
|
||||
|
||||
Unlock the agent's ability to modify code and run commands.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `app/tools/filesystem.py` | `write_file`, `make_dir`, `delete_file` (additions to existing module) |
|
||||
| `app/tools/edit.py` | `str_replace` (unique-match required), `patch_apply` |
|
||||
| `app/tools/shell.py` | `run_command` with command allow/deny lists and output truncation |
|
||||
|
||||
**All write/shell operations gated through permissions service.**
|
||||
|
||||
**Exit criteria:** Agent can autonomously create files, edit code via string replacement, and run shell commands — all with user approval for destructive operations.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Polish and Hardening
|
||||
|
||||
Production-readiness: error handling, resource limits, and documentation.
|
||||
|
||||
| Area | Description |
|
||||
|------|-------------|
|
||||
| Error handling | Recovery from malformed tool calls, LLM errors, network timeouts in agent loop |
|
||||
| Token budget | Conversation truncation or summarization when approaching context limit |
|
||||
| Graceful shutdown | Clean Ctrl+C handling, session state preservation |
|
||||
| Testing | End-to-end integration tests (`tests/integration/`), unit tests (`tests/unit/`) |
|
||||
| Documentation | `README.md` with setup and usage instructions, `docs/tools.md` tool reference |
|
||||
|
||||
**Exit criteria:** Agent handles edge cases gracefully, tests pass, and a new user can set up and use the project from the README alone.
|
||||
|
||||
---
|
||||
|
||||
## File Coverage
|
||||
|
||||
Every file from the project structure in CLAUDE.md is accounted for:
|
||||
|
||||
| File | Phase |
|
||||
|------|-------|
|
||||
| `app/main.py` | 1, 2 |
|
||||
| `app/models/config.py` | 1 |
|
||||
| `app/models/message.py` | 1 |
|
||||
| `app/models/tool_call.py` | 1 |
|
||||
| `app/utils/logging.py` | 1 |
|
||||
| `app/utils/display.py` | 1, 2 |
|
||||
| `app/utils/file_helpers.py` | 1 |
|
||||
| `app/utils/token_counter.py` | 1 |
|
||||
| `app/agent/context.py` | 2 |
|
||||
| `app/services/llm.py` | 3 |
|
||||
| `app/services/streaming.py` | 3 |
|
||||
| `app/tools/base.py` | 4 |
|
||||
| `app/tools/registry.py` | 4 |
|
||||
| `app/services/permissions.py` | 4 |
|
||||
| `app/tools/filesystem.py` | 4, 6 |
|
||||
| `app/tools/search.py` | 4 |
|
||||
| `app/agent/loop.py` | 5 |
|
||||
| `app/tools/edit.py` | 6 |
|
||||
| `app/tools/shell.py` | 6 |
|
||||
1802
docs/superpowers/plans/2026-03-11-textual-tui.md
Normal file
1802
docs/superpowers/plans/2026-03-11-textual-tui.md
Normal file
File diff suppressed because it is too large
Load Diff
192
docs/superpowers/specs/2026-03-11-textual-tui-design.md
Normal file
192
docs/superpowers/specs/2026-03-11-textual-tui-design.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Textual TUI Redesign — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the current sequential print-and-scroll terminal UI with a full persistent split-screen TUI using Textual. Input is pinned at the bottom, scrollable message history above, with a header showing app/model info and a footer showing token usage and iteration count.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
+------------------- Header --------------------+
|
||||
| SneakyCode qwen2.5-coder:32b |
|
||||
+-----------------------------------------------+
|
||||
| |
|
||||
| +--- You ---+ |
|
||||
| | prompt | <- RichLog widget |
|
||||
| +-----------+ (handles own scrolling) |
|
||||
| |
|
||||
| Thinking... |
|
||||
| |
|
||||
| +-- Assistant --+ |
|
||||
| | response... | |
|
||||
| +---------------+ |
|
||||
| |
|
||||
| > read_file README.md -- 148 lines, 5128 ch |
|
||||
| > grep_files "pattern" -- 3 matches |
|
||||
| |
|
||||
+-----------------------------------------------+
|
||||
| Tokens: ~1,511 / 32,000 | Iteration 5/25 | <- StatusBar
|
||||
+-----------------------------------------------+
|
||||
| > [input cursor] | <- Input widget
|
||||
+-----------------------------------------------+
|
||||
```
|
||||
|
||||
**Widget hierarchy (no VerticalScroll wrapper — RichLog handles its own scrolling):**
|
||||
- `Header` — Textual built-in, title="SneakyCode", subtitle=model name
|
||||
- `RichLog` (id="chat-log") — main scroll area, accepts Rich renderables via `.write()`
|
||||
- `StreamingStatic` — persistent hidden `Static` widget, shown/hidden during streaming (avoids mount/unmount overhead)
|
||||
- `StatusBar` — custom `Static` widget, 1 row, docked above Input
|
||||
- `Input` — Textual built-in, pinned at bottom
|
||||
|
||||
## New Files
|
||||
|
||||
### `app/ui/app.py` — Textual App
|
||||
|
||||
SneakyCodeApp subclasses `textual.app.App`. Responsibilities:
|
||||
|
||||
- `compose()` yields: Header, RichLog(id="chat-log"), StreamingStatic(id="streaming"), StatusBar(id="status"), Input
|
||||
- `on_input_submitted()` handler: reads input value, clears input, writes user panel to chat log, dispatches agent turn as a worker
|
||||
- Agent turn runs via `run_worker()` (async worker, NOT threaded) so the UI stays responsive. Since the worker is async and on the event loop, widget methods can be called directly — no `call_from_thread()` needed.
|
||||
- Slash commands (/quit, /history, /clear, /save, /session) parsed from input before dispatching to agent
|
||||
- Holds references to config, SessionContext, AgentLoop (created in `on_mount`)
|
||||
- Header subtitle set to model name from config
|
||||
- `on_worker_state_changed()` handler: catches worker errors and writes error panels to RichLog
|
||||
- Ctrl+C binding: cancels the running agent worker (does NOT quit the app). A second Ctrl+C or `/quit` exits.
|
||||
|
||||
### `app/ui/widgets.py` — Custom Widgets
|
||||
|
||||
**StatusBar** — A simple `Static` widget styled as a footer bar. Displays token usage and iteration count. Updated by the agent loop after each LLM step via `status_bar.update(renderable)`.
|
||||
|
||||
**StreamingStatic** — A `Static` widget that stays mounted but hidden. During streaming, it becomes visible and receives `update()` calls with partial content. When streaming ends, it is hidden and its content is cleared. This avoids the overhead of mounting/unmounting on every LLM response.
|
||||
|
||||
### `app/ui/styles.tcss` — Textual CSS
|
||||
|
||||
Layout rules:
|
||||
- RichLog fills available height (fraction-based sizing, e.g. `height: 1fr`)
|
||||
- StreamingStatic: `display: none` by default, shown during streaming
|
||||
- StatusBar is 1 row, docked bottom above Input
|
||||
- Input is 1 row, docked at very bottom
|
||||
- Color scheme matches existing SNEAKYCODE_THEME (cyan for user, green for assistant, magenta for tools, dim for metadata)
|
||||
|
||||
## Modified Files
|
||||
|
||||
### `app/main.py`
|
||||
|
||||
- Remove `_run_repl()` async function entirely
|
||||
- Remove `console.input()` usage
|
||||
- `main()` creates config, runs preflight via `asyncio.run(_preflight(config))` (before Textual starts — this is fine, separate event loop), then instantiates and runs `SneakyCodeApp(config).run()`
|
||||
- CLI arg parsing stays (--config, -v, --log-file)
|
||||
- Session resume: `_offer_session_resume()` moves into `SneakyCodeApp.on_mount()` — instead of `console.input()`, push a modal screen asking "Resume previous session? [y/n]" with button/key handlers
|
||||
- Auto-save: triggers after each agent turn completes (in the worker completion handler)
|
||||
- SIGTERM handler: removed — Textual manages its own signal handling and shutdown lifecycle
|
||||
|
||||
### `app/services/streaming.py`
|
||||
|
||||
- Remove `from rich.live import Live` and `from rich.spinner import Spinner`
|
||||
- `process_stream()` no longer creates a `Rich.Live` context
|
||||
- Instead, accepts callback parameters:
|
||||
- `on_content: Callable[[str], None]` — called with accumulated content on each content chunk
|
||||
- `on_thinking: Callable[[], None]` — called once when first reasoning token arrives
|
||||
- `on_done: Callable[[], None]` — called when streaming completes
|
||||
- **Throttling:** Content callback fires at most every 100ms (track last update time, skip intermediate chunks). Final content always fires on stream end.
|
||||
- Since the agent runs as an async worker (on the event loop), callbacks can directly call widget methods — no `call_from_thread()` needed.
|
||||
- All accumulation and tool-call parsing logic stays identical
|
||||
|
||||
### `app/utils/display.py`
|
||||
|
||||
- All `print_*` functions become `render_*` functions that return Rich renderables:
|
||||
- `render_user_message(content) -> Panel`
|
||||
- `render_assistant_message(content) -> Panel`
|
||||
- `render_tool_call(name, args) -> Text`
|
||||
- `render_tool_result(name, output, is_error) -> Text`
|
||||
- `render_iteration_header(iteration, max_iter) -> Text`
|
||||
- `render_warning(message) -> Text`
|
||||
- `render_error(message) -> Text`
|
||||
- `print_banner()` removed — Header widget replaces it
|
||||
- `print_token_usage()` becomes `render_token_usage() -> Text` for the StatusBar
|
||||
- `print_history()` becomes `render_history() -> Table` — written to RichLog, may need width constraints for narrow terminals
|
||||
- A `DisplayAdapter` class wraps a `RichLog` reference and provides `write_user_message()`, `write_tool_call()`, etc. methods that call `render_*` then `rich_log.write()`
|
||||
|
||||
### `app/agent/loop.py`
|
||||
|
||||
- `AgentLoop.__init__()` accepts a `DisplayAdapter` instead of calling `display.py` print functions directly
|
||||
- All display calls route through the adapter: `self._display.write_tool_call(name, args)`, `self._display.write_iteration_header(i, max)`, etc.
|
||||
- `_execute_tool_calls()` becomes `async def _execute_tool_calls()` to support async permission checks
|
||||
- The loop logic (ReAct pattern, retry, truncation) is unchanged
|
||||
|
||||
### `app/services/permissions.py`
|
||||
|
||||
- `PermissionsService.check()` becomes `async def check()`
|
||||
- Instead of `rich.prompt.Confirm.ask()` (blocking stdin read), it:
|
||||
1. Creates an `asyncio.Event`
|
||||
2. Posts a custom message to the app requesting a permission modal
|
||||
3. The app pushes a modal screen with the permission question and approve/deny buttons
|
||||
4. When the user responds, the modal sets the event and stores the result
|
||||
5. `check()` awaits the event and reads the result
|
||||
- Edge cases: dismiss without choosing = deny. Ctrl+C during modal = deny. Focus returns to Input after modal dismisses.
|
||||
|
||||
### `app/utils/logging.py`
|
||||
|
||||
- **Critical change:** The shared `console = Console()` instance will corrupt the Textual display since Textual takes exclusive terminal control
|
||||
- When running under Textual: disable `RichHandler` (console handler), keep only the file handler
|
||||
- Add a `setup_logging_for_tui()` function that reconfigures logging to file-only mode
|
||||
- Called from `SneakyCodeApp.on_mount()` before any agent work begins
|
||||
- The `console` object still exists but should not be used for output during TUI mode — all output goes through the DisplayAdapter
|
||||
- Consider: `--log-file` becomes required (or auto-set to a default) when running in TUI mode, so logs are not lost
|
||||
|
||||
## Unchanged Files
|
||||
|
||||
- `app/services/llm.py` — HTTP client, SSE parsing untouched
|
||||
- `app/agent/context.py` — session state untouched
|
||||
- `app/models/*` — all data models untouched
|
||||
- `app/tools/*` — all tool implementations untouched
|
||||
- `app/utils/file_helpers.py` — path safety untouched
|
||||
- `app/utils/token_counter.py` — token counting untouched
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Streaming in Textual
|
||||
|
||||
The agent loop runs as an async worker (on the event loop, NOT threaded). During streaming:
|
||||
|
||||
1. App shows `StreamingStatic` widget, writes "Thinking..." initially
|
||||
2. Worker calls `StreamHandler.process_stream(chunks, on_content=..., on_thinking=..., on_done=...)`
|
||||
3. `on_content` callback: updates `StreamingStatic` with `Panel(Markdown(partial_content), title="Assistant", border_style="green")` — throttled to ~100ms intervals
|
||||
4. `on_done` callback: hides `StreamingStatic`, writes final content to `RichLog` via `DisplayAdapter`
|
||||
|
||||
Since the worker is async (not threaded), callbacks run on the event loop and can call widget methods directly.
|
||||
|
||||
### Permission Prompts
|
||||
|
||||
1. Agent loop (in async worker) calls `await permissions.check(operation, details)`
|
||||
2. `check()` creates an `asyncio.Event` and posts `PermissionRequest` message to the app
|
||||
3. App handles `PermissionRequest`: pushes a modal screen with the question, approve/deny buttons
|
||||
4. Modal screen: on button press, stores result and sets the event
|
||||
5. `check()` awaits the event, reads result, returns approved/denied
|
||||
6. Focus management: Input loses focus when modal appears, regains focus when modal dismisses
|
||||
7. Default on dismiss/Ctrl+C: deny
|
||||
|
||||
### Cancellation
|
||||
|
||||
- Ctrl+C (first press): cancels the running agent worker via `worker.cancel()`. The agent loop should check for cancellation between iterations.
|
||||
- Ctrl+C (second press) or `/quit`: exits the app via `app.exit()`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Add `textual>=4.0.0` to pyproject.toml dependencies
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run the app — header shows app name + model, no console corruption
|
||||
2. Type a prompt — user panel appears in scroll area, input clears
|
||||
3. During LLM streaming — assistant response types out live (throttled) in the scroll area
|
||||
4. Thinking indicator shows during reasoning-only phases
|
||||
5. Tool calls appear as compact lines in the scroll area
|
||||
6. Footer shows token usage and iteration count, updating each step
|
||||
7. Scroll area auto-scrolls to bottom on new content
|
||||
8. /quit, /clear, /history commands work from the input
|
||||
9. Permission prompts show as modal, approve/deny work, focus returns to input
|
||||
10. Ctrl+C cancels running agent turn without quitting
|
||||
11. Worker errors display as error panels in the scroll area
|
||||
12. Logging goes to file only — no console corruption
|
||||
13. Session resume works on startup via modal dialog
|
||||
@@ -1 +1,21 @@
|
||||
Pressing up should cycle history like claude code.
|
||||
|
||||
~~Remove the user's input from output "you" - it's not needed.~~ Brought back as a condensed one-liner (first line + line count for multi-line).
|
||||
|
||||
Smart shell auto-approve: auto-approve `run_command` when the base executable is in the `allowed_commands` list and the full command doesn't match any `denied_commands` prefix. Only prompt the user for commands whose base executable is unlisted. Currently all shell commands prompt regardless, which is tedious for safe read-only commands like `git branch` or `ls`. The allow/deny lists in `ShellToolConfig` already define what's safe — the permissions service just needs to be shell-aware.
|
||||
|
||||
Show a token count or some other display for when the model is "thinking" for a long period of time. I want a way for the user to know the model is working on it.
|
||||
|
||||
/models command to show models available and temporarly change models in the session
|
||||
|
||||
pass a directory to the tool so that it uses that directory as it's root for commands.
|
||||
|
||||
add a skills directory so we can prompt our own skills for the tool to use similar to Claude Code
|
||||
|
||||
need not only a session log, but also a log of what the llm is thinking and how it's working somehow. I need a way to see behind the curtain.
|
||||
|
||||
# Left of from Phase 7 of old roadmap - finish these first
|
||||
- Permission modal auto-approves (TODO: proper modal dialog)
|
||||
- Session resume auto-resumes (TODO: modal y/n)
|
||||
- LLM client cleanup on unmount not yet wired
|
||||
- No automated TUI tests (Textual's AppTest can be added later)
|
||||
Reference in New Issue
Block a user