-
-
Notifications
You must be signed in to change notification settings - Fork 826
feat: Client-tool execution + permission system #3370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 29 commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
615ed10
wip: tool perms
BinaryMuse fced2b8
wip: tool perms
BinaryMuse 64b8247
wip: permission system
BinaryMuse d605c90
Make most of atuin-ai crate pub(crate)
BinaryMuse 3429f87
wip
BinaryMuse 424f41f
atuin-ai: Phase 1 — establish foundation types and resolve mutation b…
BinaryMuse 66cd10c
atuin-ai: Phase 2 — decompose state, extract dispatch, split stream f…
BinaryMuse 339dcb8
atuin-ai: Phase 3 — extract tool execution pipeline
BinaryMuse 2d3885c
Close the client-side tool call loop
BinaryMuse 1b84eb6
Remove temp debugging
BinaryMuse 5697d9b
Ensure all local tool calls resolve before resuming stream
BinaryMuse d52ada1
Implement AtuinHistory client-side tool execution
BinaryMuse c29add2
Remove permissions file
BinaryMuse 885d32a
ignore permissions files
BinaryMuse 0ecfa74
Add timestamp and duration to AtuinHistory results
BinaryMuse 5c8b22b
Add tree-sitter shell command parsing for scoped permissions
BinaryMuse 4271f20
Stress-test shell parsing and fix concatenation gap
BinaryMuse e55d898
implement client-side shell command execution with VT100 preview
BinaryMuse 04ef52c
Fix bug in events -> messages conversion
BinaryMuse 45f4170
Introduce ToolTracker as single source of truth for tool execution state
BinaryMuse cb6b12d
Change client capabilities to individual tools
BinaryMuse d380aa3
Fix list item rendering
BinaryMuse 0579001
Only allow history search by defualt
BinaryMuse 8c60551
Fix tool calls when permissions needed
BinaryMuse a288f53
Handle tool call decline flow
BinaryMuse 2007701
Fix spinners not spinning
BinaryMuse 61a7b95
Clippy
BinaryMuse 72c0316
Merge remote-tracking branch 'origin/main' into mkt/tool-perms
BinaryMuse 8e9619e
more clippy
BinaryMuse 28879bb
Implement 'always allow' and 'always allow in dir/project'
BinaryMuse 7339cf8
Change read_file to use offset and limit
BinaryMuse 45ae07e
Properly handle rule scopes in Read and Write tools
BinaryMuse 2dfa30b
Tweak rule file application order
BinaryMuse dc8bf4d
Don't branch on runtime if no AI features
BinaryMuse bc1415c
Code review feedback
BinaryMuse 557c2d5
Gate TreeSitter behind *nix
BinaryMuse b909bd8
feedback
BinaryMuse 85f76b0
Fix Atuin AI compile on Windows
BinaryMuse d46f22c
Only enable treesitter on linux/macos target_os
BinaryMuse 2c69d70
Gate tests that require tree-sitter behind flag
BinaryMuse 6bac5d9
Don't execute client tools if capability wasn't advertised
BinaryMuse 48c1266
Clean up server descriptors
BinaryMuse 346066e
Convert paths on Windows for tool Rule matching
BinaryMuse 4c965e3
Split path glob tests into windows and non-windows
BinaryMuse 97e667c
Use capabilities from config to control req caps
BinaryMuse e79a44d
Add docs for new capabilities settings
BinaryMuse dc97331
and I type for a living
BinaryMuse d28afa4
Add instructions for disabling AI keybind
BinaryMuse 8985e3d
If ai.enabled is None, show prompt to enable, disable, or cancel
BinaryMuse c5c76c4
Fine
BinaryMuse c426f11
Remove accidental files
BinaryMuse e8e90af
Update crates/atuin-client/src/settings.rs
BinaryMuse ee1c531
Use same vt100 project-wide
BinaryMuse add78e6
Merge branch 'mkt/tool-perms' of github.com:atuinsh/atuin into mkt/to…
BinaryMuse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,3 +13,5 @@ ui/backend/target | |
| ui/backend/gen | ||
|
|
||
| sqlite-server.db* | ||
|
|
||
| .atuin/permissions.*.toml | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| # Plan: Add Table Rendering to atuin-ai Markdown | ||
|
|
||
| ## Context | ||
|
|
||
| The `Markdown` component (`crates/atuin-ai/src/tui/components/markdown.rs`) renders AI | ||
| responses using pulldown-cmark, but drops table events silently (`_ => {}` at line 209). | ||
| pulldown-cmark supports GFM tables via `Options::ENABLE_TABLES`. The goal is to render | ||
| markdown tables as proper terminal tables. | ||
|
|
||
| ## Architecture: Composite `#[component]` returning stacked Elements | ||
|
|
||
| Convert `Markdown` from a `#[props]` + manual `impl Component` leaf to a `#[component]` | ||
| function returning `Elements`. Each markdown block becomes a child element: | ||
|
|
||
| - **Text blocks** → `Canvas` leaf wrapping `Paragraph` (with width-aware | ||
| `desired_height_fn` — no probe render needed) | ||
| - **Table blocks** → `View` containing stacked `HStack` rows, each with `Column` | ||
| children for cells. `WidthConstraint::Fill` gives equal column widths; the framework's | ||
| `allocate_widths` handles distribution. | ||
|
|
||
| This matches the existing pattern used by eye_declare's built-in `Markdown` component | ||
| (`eye_declare/src/components/markdown.rs:99-118`) and the `Spinner` component. | ||
|
|
||
| Why this approach over a leaf component: | ||
| 1. **Streaming**: AI responses stream in, so markdown re-renders frequently. The | ||
| `#[component]` function runs during reconciliation (fast parse), while rendering is | ||
| deferred and cached per-node. Canvas reports height via `desired_height_fn` — no | ||
| probe render fallback. | ||
| 2. **Accurate heights**: Table rows are composites — the framework measures children | ||
| automatically. No manual height estimation. | ||
| 3. **Natural layout**: `HStack` + `Column` with `Fill` widths gives equal columns for | ||
| free. Can later measure content for `Fixed(n)` widths. | ||
|
|
||
| ## Files to modify | ||
|
|
||
| 1. **`crates/atuin-ai/src/tui/components/markdown.rs`** — all changes | ||
| 2. No dependency or Cargo.toml changes needed | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Step 1: Define block types | ||
|
|
||
| Add an internal enum for parsed markdown blocks: | ||
|
|
||
| ```rust | ||
| enum MarkdownBlock { | ||
| Text(Vec<Line<'static>>), | ||
| Table { | ||
| headers: Vec<Vec<Span<'static>>>, | ||
| rows: Vec<Vec<Vec<Span<'static>>>>, // row → cell → spans | ||
| alignments: Vec<pulldown_cmark::Alignment>, | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2: Refactor `parse_markdown` → `parse_markdown_blocks` | ||
|
|
||
| Returns `Vec<MarkdownBlock>` instead of `Text<'static>`. | ||
|
|
||
| Enable table parsing: `Parser::new_ext(source, Options::ENABLE_TABLES)`. Add `Options` to | ||
| the `pulldown_cmark` import. | ||
|
|
||
| The existing event loop handles inline formatting (bold, italic, code) via the style | ||
| stack — this works identically inside table cells. New logic: | ||
|
|
||
| - Track `in_table`, `in_table_head`, `in_table_row`, `in_table_cell` bools | ||
| - Accumulate cell content as `Vec<Span<'static>>` per cell | ||
| - On `Event::End(TagEnd::TableCell)`: push cell to current row vec | ||
| - On `Event::End(TagEnd::TableRow)`: push row to table rows vec | ||
| - On `Event::End(TagEnd::Table)`: emit `MarkdownBlock::Table`, reset table state | ||
| - At block boundaries (paragraph, heading, code block, list) outside tables: flush | ||
| accumulated lines as `MarkdownBlock::Text` | ||
|
|
||
| pulldown-cmark table event structure: | ||
| ``` | ||
| Start(Table(Vec<Alignment>)) | ||
| Start(TableHead) | ||
| Start(TableRow) | ||
| Start(TableCell) Text(...) End(TableCell) | ||
| End(TableRow) | ||
| End(TableHead) | ||
| Start(TableRow) // body rows, no TableBody wrapper | ||
| Start(TableCell) Text(...) End(TableCell) | ||
| End(TableRow) | ||
| End(Table) | ||
| ``` | ||
|
|
||
| ### Step 3: Convert to `#[component]` function | ||
|
|
||
| Keep `#[props]` on the struct (generates builder for `element!` macro). Remove the manual | ||
| `impl Component for Markdown` block. Add a `#[component]` function: | ||
|
|
||
| ```rust | ||
| #[eye_declare_macros::component( | ||
| props = Markdown, | ||
| state = MarkdownStyles, | ||
| initial_state = MarkdownStyles::new(), | ||
| crate_path = eye_declare, | ||
| )] | ||
| fn markdown(props: &Markdown, styles: &MarkdownStyles) -> Elements { | ||
| if props.source.is_empty() { | ||
| return Elements::new(); | ||
| } | ||
| let blocks = parse_markdown_blocks(&props.source, styles); | ||
| let mut els = Elements::new(); | ||
| for block in blocks { | ||
| match block { | ||
| MarkdownBlock::Text(lines) => { | ||
| // Canvas with Paragraph — same pattern as eye_declare's built-in Markdown | ||
| let text = Text::from(lines); | ||
| let text_for_height = text.clone(); | ||
| els.add( | ||
| Canvas::builder() | ||
| .render_fn(move |area: Rect, buf: &mut Buffer| { | ||
| Paragraph::new(text.clone()) | ||
| .wrap(Wrap { trim: false }) | ||
| .render(area, buf); | ||
| }) | ||
| .desired_height_fn(move |width: u16| { | ||
| Paragraph::new(text_for_height.clone()) | ||
| .wrap(Wrap { trim: false }) | ||
| .line_count(width) as u16 | ||
| }) | ||
| .build(), | ||
| ); | ||
| } | ||
| MarkdownBlock::Table { headers, rows, .. } => { | ||
| els.add(table_element(headers, rows)); | ||
| } | ||
| } | ||
| } | ||
| els | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 4: Build table elements with HStack/Column | ||
|
|
||
| The `table_element` function builds a `View` child tree: | ||
|
|
||
| ```rust | ||
| fn table_element( | ||
| headers: Vec<Vec<Span<'static>>>, | ||
| rows: Vec<Vec<Vec<Span<'static>>>>, | ||
| ) -> Elements { | ||
| let num_cols = headers.len().max(rows.first().map_or(0, |r| r.len())); | ||
|
|
||
| element! { | ||
| View(padding_top: Cells::from(1), padding_bottom: Cells::from(1)) { | ||
| // Header row | ||
| HStack { | ||
| #(for header_cell in &headers { | ||
| Column { | ||
| Text { | ||
| #(for span in header_cell.clone() { | ||
| Span( | ||
| text: span.content.to_string(), | ||
| style: span.style.add_modifier(Modifier::BOLD), | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
| }) | ||
| // Pad remaining columns if headers < num_cols | ||
| #(for _ in headers.len()..num_cols { | ||
| Column { Text { "" } } | ||
| }) | ||
| } | ||
| // Data rows | ||
| #(for row in &rows { | ||
| HStack { | ||
| #(for cell_spans in row { | ||
| Column { | ||
| Text { | ||
| #(for span in cell_spans.clone() { | ||
| Span( | ||
| text: span.content.to_string(), | ||
| style: span.style, | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
| }) | ||
| #(for _ in row.len()..num_cols { | ||
| Column { Text { "" } } | ||
| }) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Each `Column` defaults to `WidthConstraint::Fill`, so the framework allocates equal width | ||
| per column. The `View` wrapper adds vertical padding around the table. | ||
|
|
||
| ### Step 5: Update imports | ||
|
|
||
| ```rust | ||
| use eye_declare::{ | ||
| Cells, Canvas, Column, Component, Elements, HStack, Span, Text, View, element, props, | ||
| }; | ||
| use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; | ||
| ``` | ||
|
|
||
| Remove: `use ratatui_core::widgets::Widget;` (no longer rendering directly; Canvas and | ||
| the framework handle it). | ||
|
|
||
| ### Step 6: Remove old code | ||
|
|
||
| Delete: | ||
| - `impl Component for Markdown { ... }` block (lines 63–91) | ||
| - `fn parse_markdown(...)` (lines 94–215) — replaced by `parse_markdown_blocks` | ||
| - `MarkdownStyles::initial_state()` on the Component impl (handled by `#[component]`) | ||
|
|
||
| ## Edge cases | ||
|
|
||
| - **Uneven rows**: Pad shorter rows with empty `Column` children (shown above) | ||
| - **Empty tables**: Skip (don't emit element) | ||
| - **Column spacing**: Start without spacer columns (content provides natural whitespace). | ||
| Add `Fixed(1)` spacer Columns later if too cramped. | ||
| - **Inline formatting in cells**: Handled by existing style stack during parsing — bold, | ||
| italic, code inside cells all work | ||
| - **Wide tables**: Columns compress equally; content may truncate (acceptable for | ||
| terminal). Content-based column sizing is a future enhancement. | ||
|
|
||
| ## Verification | ||
|
|
||
| ```sh | ||
| cargo build -p atuin-ai | ||
| cargo clippy -p atuin-ai -- -D warnings | ||
| cargo test -p atuin-ai | ||
| cargo fmt --check | ||
| ``` | ||
|
|
||
| Add unit tests: | ||
| - Parse a table markdown string and verify block structure | ||
| - Render a table into a buffer and verify cell content at expected positions | ||
|
|
||
| ## Future enhancements (out of scope) | ||
|
|
||
| - **Content-based column sizing**: Measure max cell width per column, use | ||
| `WidthConstraint::Fixed(n)` for tighter layout | ||
| - **Column alignment**: Map `pulldown_cmark::Alignment::{Left, Center, Right}` to text | ||
| alignment | ||
| - **Strikethrough/blockquote**: Other pulldown-cmark extensions | ||
| - **Column spacer**: `Fixed(1)` spacer Columns between data columns for visual separation | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.