10 Commits

Author SHA1 Message Date
Edward Langley c3ae848f54 chore(merge): branch 'main' into command-algebra 2026-04-07 00:10:19 -07:00
Edward Langley 1d5a04088b chore: reformat 2026-04-07 00:09:58 -07:00
Edward Langley a330412732 chore(merge): remote-tracking branch 'gh/main' 2026-04-07 00:02:29 -07:00
Ed Langley 43015a41d8 Merge pull request #1 from fiddlerwoaroof/add-claude-github-actions-1775545235526
Add Claude Code GitHub Workflow
2026-04-07 00:00:59 -07:00
Ed Langley 29f922aaca "Claude Code Review workflow" 2026-04-07 00:00:39 -07:00
Ed Langley ee6970fed0 "Claude PR Assistant workflow" 2026-04-07 00:00:37 -07:00
Claude 880c471ff6 refactor(cmd): introduce command algebra with Binding::Sequence and unified primitives
Add Binding::Sequence to keymap for composing commands, then use it
and parameterization to eliminate redundant command structs:

- Unify MoveSelection/JumpToEdge/ScrollRows/PageScroll into Move
- Merge ToggleGroupUnderCursor + ToggleColGroupUnderCursor → ToggleGroupAtCursor
- Merge CommitCellEdit + CommitAndAdvanceRight → CommitAndAdvance
- Merge CycleAxisForTile + SetAxisForTile → TileAxisOp
- Merge ViewBackCmd + ViewForwardCmd → ViewNavigate
- Delete SearchAppendChar/SearchPopChar (reuse AppendChar/PopChar with "search")
- Replace SaveAndQuit/OpenRecordRow with keymap sequences
- Extract commit_add_from_buffer helper for CommitCategoryAdd/CommitItemAdd
- Add algebraic law tests (idempotence, involution, associativity)

https://claude.ai/code/session_01Y9X6VKyZAW3xo1nfThDRYU
2026-04-07 06:48:34 +00:00
Edward Langley e166049bae chore: clippy 2026-04-06 23:21:36 -07:00
Edward Langley 2c05b64d51 misc 2026-04-06 23:21:06 -07:00
Edward Langley 86ac30c11d refactor(ui): ensure layout is rebuilt and use Rc for drill state
Update UI components and effects to ensure layout is rebuilt when necessary.

- Rebuild layout in `EnterEditAtCursor` effect
- Use `Rc` for sharing records in `StartDrill` effect
- Improve formatting in `SetDrillPendingEdit` effect
- Update test case in `App` to use multi-line method calls

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
2026-04-06 23:18:40 -07:00
9 changed files with 557 additions and 415 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
+50
View File
@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
+380 -364
View File
File diff suppressed because it is too large Load Diff
+33 -4
View File
@@ -72,6 +72,8 @@ pub enum Binding {
}, },
/// A prefix sub-keymap (Emacs-style). /// A prefix sub-keymap (Emacs-style).
Prefix(Arc<Keymap>), Prefix(Arc<Keymap>),
/// A sequence of commands executed in order, concatenating their effects.
Sequence(Vec<(&'static str, Vec<String>)>),
} }
/// A keymap maps key patterns to bindings (command names or prefix sub-keymaps). /// A keymap maps key patterns to bindings (command names or prefix sub-keymaps).
@@ -121,6 +123,17 @@ impl Keymap {
.insert(KeyPattern::Key(key, mods), Binding::Prefix(sub)); .insert(KeyPattern::Key(key, mods), Binding::Prefix(sub));
} }
/// Bind a key to a sequence of commands (executed in order).
pub fn bind_seq(
&mut self,
key: KeyCode,
mods: KeyModifiers,
steps: Vec<(&'static str, Vec<String>)>,
) {
self.bindings
.insert(KeyPattern::Key(key, mods), Binding::Sequence(steps));
}
/// Bind a catch-all for any Char key. /// Bind a catch-all for any Char key.
pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) { pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) {
self.bindings self.bindings
@@ -173,6 +186,14 @@ impl Keymap {
Some(cmd.execute(ctx)) Some(cmd.execute(ctx))
} }
Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]), Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]),
Binding::Sequence(steps) => {
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
for (name, args) in steps {
let cmd = registry.interactive(name, args, ctx).ok()?;
effects.extend(cmd.execute(ctx));
}
Some(effects)
}
} }
} }
} }
@@ -360,7 +381,11 @@ impl KeymapSet {
// Drill into aggregated cell / view history / add row // Drill into aggregated cell / view history / add row
normal.bind(KeyCode::Char('>'), none, "drill-into-cell"); normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
normal.bind(KeyCode::Char('<'), none, "view-back"); normal.bind(KeyCode::Char('<'), none, "view-back");
normal.bind(KeyCode::Char('o'), none, "open-record-row"); normal.bind_seq(
KeyCode::Char('o'),
none,
vec![("add-record-row", vec![]), ("enter-edit-at-cursor", vec![])],
);
// Records mode toggle and prune toggle // Records mode toggle and prune toggle
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode"); normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
@@ -384,7 +409,11 @@ impl KeymapSet {
normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map)); normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map));
let mut z_map = Keymap::new(); let mut z_map = Keymap::new();
z_map.bind(KeyCode::Char('Z'), none, "save-and-quit"); z_map.bind_seq(
KeyCode::Char('Z'),
none,
vec![("save", vec![]), ("force-quit", vec![])],
);
normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map)); normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));
set.insert(ModeKey::Normal, Arc::new(normal)); set.insert(ModeKey::Normal, Arc::new(normal));
@@ -664,8 +693,8 @@ impl KeymapSet {
let mut sm = Keymap::new(); let mut sm = Keymap::new();
sm.bind(KeyCode::Esc, none, "exit-search-mode"); sm.bind(KeyCode::Esc, none, "exit-search-mode");
sm.bind(KeyCode::Enter, none, "exit-search-mode"); sm.bind(KeyCode::Enter, none, "exit-search-mode");
sm.bind(KeyCode::Backspace, none, "search-pop-char"); sm.bind_args(KeyCode::Backspace, none, "pop-char", vec!["search".into()]);
sm.bind_any_char("search-append-char", vec![]); sm.bind_any_char("append-char", vec!["search".into()]);
set.insert(ModeKey::SearchMode, Arc::new(sm)); set.insert(ModeKey::SearchMode, Arc::new(sm));
// ── Import wizard ──────────────────────────────────────────────── // ── Import wizard ────────────────────────────────────────────────
+4 -1
View File
@@ -506,7 +506,10 @@ mod tests {
let mut app = two_col_model(); let mut app = two_col_model();
// Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12. // Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12.
for i in 0..10 { for i in 0..10 {
app.model.category_mut("Row").unwrap().add_item(&format!("R{i}")); app.model
.category_mut("Row")
.unwrap()
.add_item(&format!("R{i}"));
} }
app.term_height = 13; // ~5 visible rows app.term_height = 13; // ~5 visible rows
app.model.active_view_mut().selected = (0, 0); app.model.active_view_mut().selected = (0, 0);
+9 -4
View File
@@ -96,7 +96,11 @@ impl Effect for RemoveFormula {
pub struct EnterEditAtCursor; pub struct EnterEditAtCursor;
impl Effect for EnterEditAtCursor { impl Effect for EnterEditAtCursor {
fn apply(&self, app: &mut App) { fn apply(&self, app: &mut App) {
let ctx = app.cmd_context(crossterm::event::KeyCode::Null, crossterm::event::KeyModifiers::NONE); app.rebuild_layout();
let ctx = app.cmd_context(
crossterm::event::KeyCode::Null,
crossterm::event::KeyModifiers::NONE,
);
let value = ctx.display_value.clone(); let value = ctx.display_value.clone();
drop(ctx); drop(ctx);
app.buffers.insert("edit".to_string(), value); app.buffers.insert("edit".to_string(), value);
@@ -472,9 +476,10 @@ pub struct SetDrillPendingEdit {
impl Effect for SetDrillPendingEdit { impl Effect for SetDrillPendingEdit {
fn apply(&self, app: &mut App) { fn apply(&self, app: &mut App) {
if let Some(drill) = &mut app.drill_state { if let Some(drill) = &mut app.drill_state {
drill drill.pending_edits.insert(
.pending_edits (self.record_idx, self.col_name.clone()),
.insert((self.record_idx, self.col_name.clone()), self.new_value.clone()); self.new_value.clone(),
);
} }
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ impl<'a> GridWidget<'a> {
let n_col_levels = layout.col_cats.len().max(1); let n_col_levels = layout.col_cats.len().max(1);
let n_row_levels = layout.row_cats.len().max(1); let n_row_levels = layout.row_cats.len().max(1);
let col_widths = compute_col_widths(self.model, &layout, fmt_comma, fmt_decimals); let col_widths = compute_col_widths(self.model, layout, fmt_comma, fmt_decimals);
// ── Adaptive row header widths ─────────────────────────────── // ── Adaptive row header widths ───────────────────────────────
let data_row_items: Vec<&Vec<String>> = layout let data_row_items: Vec<&Vec<String>> = layout
+1 -9
View File
@@ -4,20 +4,12 @@ use ratatui::{
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::Widget, widgets::Widget,
}; };
use unicode_width::UnicodeWidthStr;
use crate::model::Model; use crate::model::Model;
use crate::ui::app::AppMode; use crate::ui::app::AppMode;
use crate::view::Axis; use crate::view::Axis;
fn axis_display(axis: Axis) -> (&'static str, Color) {
match axis {
Axis::Row => ("", Color::Green),
Axis::Column => ("", Color::Blue),
Axis::Page => ("", Color::Magenta),
Axis::None => ("", Color::DarkGray),
}
}
pub struct TileBar<'a> { pub struct TileBar<'a> {
pub model: &'a Model, pub model: &'a Model,
pub mode: &'a AppMode, pub mode: &'a AppMode,
+4 -1
View File
@@ -676,7 +676,10 @@ mod tests {
let region_col = cols.iter().position(|c| c == "Region").unwrap(); let region_col = cols.iter().position(|c| c == "Region").unwrap();
let key = layout.cell_key(0, region_col).unwrap(); let key = layout.cell_key(0, region_col).unwrap();
let display = layout.resolve_display(&key).unwrap(); let display = layout.resolve_display(&key).unwrap();
assert!(!display.is_empty(), "Region column should resolve to a value"); assert!(
!display.is_empty(),
"Region column should resolve to a value"
);
} }
#[test] #[test]