Skip to content

feat: shared file manage TUI with inline editor#344

Merged
avihut merged 44 commits intomasterfrom
feat/shared-manage
Mar 31, 2026
Merged

feat: shared file manage TUI with inline editor#344
avihut merged 44 commits intomasterfrom
feat/shared-manage

Conversation

@avihut
Copy link
Copy Markdown
Owner

@avihut avihut commented Mar 28, 2026

Summary

  • Full management TUI for shared files (daft shared manage) with tabbed interface showing all shared files across worktrees
  • Immediate-mode actions: toggle materialized/linked (m), fix symlinks (i), diff mode (d), remove files (r/Del), add files (a/+)
  • Fuzzy file finder for adding new shared files (nucleo-matcher + ignore crate for gitignore awareness)
  • Inline file editor in the preview pane (edtui with emacs-style keybindings, syntax highlighting, line numbers)
  • Deep comparison for smart materialization defaults when adding files (identical copies get linked, different copies get materialized)
  • Right-aligned status column, persistent cursor indicator, fixed info bar layout
  • Co-edited linked worktrees highlighted in green during shared file editing

Known issues

  • Shift+Tab exits editor: On some terminals, Shift+Tab sends an Esc-prefixed escape sequence that crossterm cannot reliably distinguish from a standalone Esc press, causing the inline editor to exit unexpectedly. Workaround: avoid Shift+Tab while editing.

Test plan

  • mise run clippy passes with zero warnings
  • mise run test:unit passes (960+ tests)
  • mise run test:manual -- --ci manage passes
  • Manual: open manage TUI with daft shared manage, navigate tabs/worktrees
  • Manual: toggle materialized/linked with m, verify file operations
  • Manual: enter diff mode with d, compare files across worktrees
  • Manual: add file via + tab or a, verify fuzzy search and gitignore styling
  • Manual: remove file with r, verify materialization options
  • Manual: edit file with Enter on preview, verify save on Esc
  • Manual: verify editor shows correct header (shared copy green, materialized yellow)
  • Manual: verify linked worktrees highlight green during shared file editing

Fixes #339

🤖 Generated with Claude Code

avihut and others added 30 commits March 28, 2026 18:31
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract a reusable TUI shell from the collect picker into shared_picker/,
defining a PickerMode trait that separates generic navigation/rendering
from mode-specific business logic.

New module structure:
- shared_picker/mod.rs — PickerMode trait, LoopAction, EntryDecoration
- shared_picker/state.rs — generic PickerState (navigation only)
- shared_picker/input.rs — routes navigation in shell, delegates to mode
- shared_picker/render.rs — uses mode for decorations/footer/warnings
- shared_picker/collect_mode.rs — CollectMode implementing PickerMode
- shared_picker/dialog.rs — reusable confirmation dialog
- shared_picker/shell.rs — terminal restore helper
- shared_picker/highlight.rs — unchanged syntect highlighting

The collect picker works exactly as before — same keybindings, same
rendering, same behavior. All 943 unit tests pass, all integration
tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement the core of `daft shared manage` — an interactive TUI for
inspecting and modifying shared file status (linked/materialized/
missing/conflict/broken) across all worktrees with immediate actions.

- Add WorktreeStatus enum and detect_shared_statuses() to core/shared
- Create ManageMode implementing the PickerMode trait
- Add tab_idx parameter to entry_decoration for status lookup
- Wire manage subcommand and run_manage_picker entry point
- Add 7 unit tests for status detection (linked, missing, not-collected,
  materialized, conflict, broken symlink, mixed multi-worktree)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Press `d` on a worktree entry to set it as the diff pivot, then navigate
to other worktrees to see a colored line-level diff in the preview panel.
Press `d` again or `Esc` to exit diff mode. Shows contextual messages
for identical files, missing files, and when on the pivot itself.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a confirmation modal triggered by r/Del/Backspace that lets users
remove a shared file with two options: materialize into selected
worktrees (with expandable per-worktree checklist) or delete everywhere
(with secondary confirmation). After removal, updates daft.yml,
materialized.json, and shared storage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an interactive file tree browser modal triggered by pressing 'a'
in the manage TUI. Supports directory expand/collapse, search filtering,
selecting existing files to share, and declaring new shared file paths
when no matches are found. After adding, tabs refresh automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove early return when no shared files declared — the manage TUI
  now launches with an empty tab list so users can press 'a' to add
  their first shared file.
- Add 'manage' to bash, zsh, and fish shell completion scripts for
  the 'daft shared' subcommand list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The manage TUI now always shows a + tab at the end of the tab bar.
Navigating to it and pressing Enter, Space, or a opens the add file
modal. Supports empty state where no shared files exist yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Start with TabBar focus when no real tabs exist (only the virtual +
  tab), so Enter/Space/a immediately triggers the add flow
- Down from TabBar on virtual tab goes to footer (was a no-op)
- Up from footer on virtual tab returns to TabBar (was panicking on
  current_tab() with no real tabs)
- Tab key in footer guarded against virtual tab state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… UI fixes

Rewrite the add-file modal as a full-screen fuzzy finder:
- Instant tree view on open (synchronous top-level scan)
- Background-indexed fuzzy search via nucleo-matcher with match
  position highlighting (underline)
- Gitignore-aware styling via ignore crate: orange for ignored dirs,
  white for ignored files (good sharing candidates), muted for tracked
- Expandable folders with arrow key navigation in browse mode

Align add behavior with sync/collect parity:
- Extract compute_materialization_defaults() in core/shared.rs so both
  sync and manage use the same deep-compare logic
- Identical copies across worktrees get linked, different copies get
  materialized, missing worktrees get symlinked

Fix selection legibility across all TUI components:
- Cursor row always uses bright white text on dark bg
- Tags inherit cursor bg for consistent highlight
- Remove modal: expanded worktree list uses normal color instead of dim,
  selection highlight only covers checkbox+name (not leading whitespace),
  uses white-on-orange instead of black-on-orange

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refresh_all now sets focus to WorktreeList after rebuilding tabs,
preventing the cursor from being stuck on TabBar (where Down skips
to Footer instead of navigating the worktree list).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use pattern.score() (cheap) for all entries during search, and compute
pattern.indices() (expensive) only for the ~20 visible items during
render. Fixes search lag caused by computing match positions for every
entry on every keystroke.

Add effective_score() that blends nucleo's raw score with match density
(matched chars / path length) and depth penalty, so short dense matches
like ".env" for query "env" rank above longer paths like
"src/environment.rs".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Embed an edtui-based Vim editor in the preview pane. Press Enter while
focused on the preview to start editing, Esc (in Normal mode) to save
and exit. A color-coded header shows whether the shared copy (green) or
a materialized copy (yellow) is being edited.

- Add edtui dependency with syntax-highlighting feature
- Create editor.rs with EditSession (buffer, save, render, key routing)
- Wire Enter in preview to start_edit via handle_list_key delegation
- Intercept all keys in manage event loop during editing
- Add render_editor to PickerMode trait, implement for ManageMode
- Change render chain to &mut dyn PickerMode for editor rendering
- Syntax highlighting uses base16-ocean.dark theme (matching preview)
- .env files mapped to shell syntax (matching existing highlighter)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix theme name: base16-ocean.dark -> base16-ocean-dark (edtui bundles
  its own theme set with hyphenated names)
- Add line numbers (LineNumbers::Absolute)
- Preserve bordered frame around editor (matching preview pane)
- Remove misleading :w hint from header (save is automatic on Esc)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch from Vim to emacs mode (EditorEventHandler::emacs_mode) for a
  simple editing experience: just type, arrow keys to move, Esc to exit
- Esc exits directly (no double-Esc needed)
- Dim line numbers (color 239) to distinguish from file content
- Simplify header: show "Editing shared copy" (green) or "Editing
  materialized copy (name)" (yellow), no mode indicator
- Simplify block title to just "Edit"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prepend right-aligned line numbers in dim gray (Color::Indexed(239)) to
the syntax-highlighted preview, matching the editor's line number style.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inset the editor area by 1 column to create visual padding before line
numbers, matching the preview pane's line number style.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fill the 1-column leading spacer with the same Rgb(30,30,30) background
as edtui's gutter, so it blends seamlessly with the line number column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mbers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ignore Release and Repeat key events which caused double-processing
and unresponsive behavior in emacs mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EditorState defaults to Normal mode, but emacs mode has no keybinding
to enter Insert mode. Set mode to Insert on creation, matching the
official edtui emacs example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d saves

- Remove separate header row; merge info into block title:
  "Editing shared copy" (green) or "Editing <name> (materialized)" (yellow)
- Frame border color matches the title color (green/yellow)
- "Esc: save & exit" hint in the bottom-right of the frame border
- Only write to disk if content actually changed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pad between worktree name and status tag (linked/materialized/etc.)
so tags form a right-aligned column flush with the panel edge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e len

Unicode characters like ▸ and → are multi-byte but single-column width.
Using .len() (byte count) over-estimated the left content width, causing
the right-aligned tag to jump inward on cursor rows. Switch to
.chars().count() for correct display width calculation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
avihut and others added 5 commits March 29, 2026 00:17
The spacer line between tabs and body is now always 1 row. Info/warning
messages fill this row instead of inserting a new one, preventing the
body content from shifting down when toggling materialize/link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prefix all info messages from toggle_materialize and link_entry with
the worktree name (e.g., "feat/foo: materialized (local copy created)")
so the action context is clear when navigating back to a tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pointer (▸) and white text stay visible on the current worktree
entry even when focus is on the preview or editor pane, making it clear
which worktree's file is being viewed/edited. The background highlight
only appears when the worktree list itself is focused.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The padding spaces and right-aligned status tag now also get SELECTED_BG
when the current entry is highlighted (both focused and unfocused),
so the entire row is uniformly highlighted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@avihut avihut added this to the Public Launch milestone Mar 28, 2026
@avihut avihut added the feat New feature label Mar 28, 2026
@avihut avihut self-assigned this Mar 28, 2026
avihut and others added 9 commits March 29, 2026 09:19
When the inline editor is active on a shared (linked) file, all other
linked worktrees are highlighted in green in the worktree list, making
it clear which worktrees will be affected by the edit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… exit

- Add SELECTED_BG background to co-edited linked worktrees (green text
  alone was not visible enough on dark terminals)
- Only exit editor on plain Esc with no modifiers, preventing Shift+Tab
  escape sequences from triggering an unwanted exit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The padding and status tag spans now also get SELECTED_BG for
co-edited linked worktrees, matching the full-row highlight.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…itor

Some terminals send Shift+Tab as an Esc-prefixed escape sequence that
crossterm splits into a bare Esc followed by additional characters.
After receiving Esc, peek for a follow-up event within 20ms — if one
arrives, it was part of a sequence, not a standalone Esc press.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When toggling a materialized file to linked, deep-compare the local
copy against the shared file. If they differ, show a confirmation
dialog (defaulting to No) warning that local changes will be lost.
Identical copies are linked without prompting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add vertical and horizontal padding inside the dialog border, indent
body text by 2 chars, and indent button row to match. Wider dialog
(54 cols) for better readability. Consistent with other modal overlays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove Wrap which caused padding to break on line wraps
- Auto-size dialog width to fit longest body line (no more truncation)
- Shorter body lines to avoid needing wrapping at all
- Keep dialog as overlay (Clear only clears dialog area, rest of screen
  retains previous frame content via ratatui diffing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dialog now accepts an optional background render callback (FnMut).
The link-overwrite confirmation in manage mode passes the full UI
renderer, so the dialog overlays on top of the manage interface instead
of appearing on a blank screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Modifier::DIM to all buffer cells before rendering the dialog
overlay, creating a modal backdrop effect that draws attention to the
dialog while keeping the underlying UI visible but subdued.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@avihut avihut merged commit a81fbcc into master Mar 31, 2026
8 checks passed
@avihut avihut deleted the feat/shared-manage branch March 31, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Catalog

1 participant