Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Architect solves this with a grid view that keeps all your agents visible, with
- **Dynamic grid** — starts with a single terminal in full view; press ⌘N to add a terminal after the current one, and closing terminals compacts the grid forward
- **Grid view** — keep all agents visible simultaneously, expand any one to full screen
- **Worktree picker** (⌘T) — quickly `cd` into git worktrees for parallel agent work on separate branches; new worktrees are created outside the repo tree (configurable via `[worktree]` in `config.toml`) with automatic post-create initialization
- **Recent folders** (⌘O) — quickly `cd` into recently visited directories with instant search filtering (start typing to narrow the list — the overlay takes the keyboard the instant it starts opening, so nothing you type is lost to the animation), substring highlighting, arrow key navigation, and ⌘1–⌘9 quick selection
- **Pull requests** (⌘P) — when the focused terminal is inside a GitHub-cloned repo, the ⌘P pill loads and shows the current branch's PR number (e.g. `#123`), scaled to fit the pill; expand it to list open PRs (via `gh pr list`) with search filtering and check out a branch with `gh pr checkout`
- **Recent folders** (⌘O) — quickly `cd` into recently visited directories with instant search filtering (start typing to narrow the list — the overlay takes the keyboard the instant it starts opening, so nothing you type is lost to the animation), substring highlighting, arrow key navigation, and ⌘1–⌘9 quick selection; its pill is hidden while the focused terminal runs a foreground process
- **Pull requests** (⌘P) — when the focused terminal is inside a GitHub-cloned repo, the ⌘P pill loads and shows the current branch's PR number (e.g. `#123`), scaled to fit the pill; expand it to list open PRs (via `gh pr list`) with search filtering and check out a branch with `gh pr checkout`; the pill is hidden while the focused terminal runs a foreground process
- **Diff review comments** — click diff lines in the ⌘D overlay to leave inline comments with multiline wrapping, then send them all to a running agent (or start one) with the "Send to agent" button
- **Selection-to-agent context** — release after selecting terminal text to reveal a narrow robot button anchored just outside the selection; the button follows the selection while scrolling; choose Claude, Codex, or Gemini in a centered modal with highlighted dropdown navigation, review the fully wrapped context in a scrollable field, enter wrapped multiline instructions, cancel if needed, or launch a new agent in the selected terminal's working directory with the selection submitted as its initial prompt without embedding large contexts in the shell command line
- **Story viewer** — run `architect story <filename>` to open a scrollable overlay that renders PR story files with prose text and diff-colored code blocks
Expand All @@ -43,6 +43,7 @@ Architect solves this with a grid view that keeps all your agents visible, with

### Terminal Essentials
- Smooth animated transitions for grid expansion, contraction, and reflow (cells and borders move/resize together)
- Context-aware shortcut pills stay right-aligned with fixed spacing, compact when a pill is unavailable, and animate into their new positions; newly available pills enter after the existing row reflows to avoid overlapping hit targets and remain non-interactive until they reach a clear slot; the worktree, recent-folder, and pull-request pills are hidden while the focused terminal runs a foreground process and any open picker closes immediately
- Wakeable idle input handling keeps typing responsive after short idle periods instead of waiting on a fixed sleep window
- Keyboard navigation: ⌘+Return to expand, ⌘1–⌘0 to switch grid slots, ⌘Arrow to move focus in grid view (plays a brief wave animation on the destination terminal), ⌘N to add, ⌘W to close a terminal (restarts if it's the only terminal), ⌘T for worktrees, ⌘O for recent folders, ⌘D for repo-wide git diff (staged + unstaged + untracked), ⌘R for reader mode, ⌘/ for shortcuts; quit with ⌘Q or the window close button
- Every text field (recent-folder and reader/story search, worktree name, diff comments, selection-agent instructions) behaves like a macOS text field: a blinking caret, Backspace deletes one character, ⌥Backspace the previous word, ⌘Backspace the whole field, ⌘A selects everything (the next keystroke or paste replaces it), and ⌘C/⌘V copy and paste. The selection-agent field accepts multiline instructions with ⇧↩; plain ↩ launches, while Escape or Cancel dismisses it. Pasting multi-line text into a one-line field drops the newlines, and text longer than the field fades out at its edge instead of spilling past it. Emoji are scaled to the surrounding line height wherever they appear (search fields, comments, reader mode, stories, selection-agent instructions)
Expand Down
32 changes: 31 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,35 @@ On Enter / click:
runtime.zig dispatch: send `gh pr checkout <number>\n` to the focused shell
```

### Top-Right Pill Layout

`PillGroupComponent` owns the top-right shortcut row for help, recent folders,
worktrees, and pull requests. Each overlay reports whether its pill is
currently available; the group packs available pills from right to left with a
fixed gap and applies the resulting x-coordinate to the shared
`ExpandingOverlay` geometry. When availability or the window geometry changes,
the existing pills interpolate to their new positions with the shared cubic
easing function. Newly available pills remain staged outside the row while
existing pills reflow, then enter in a second eased phase once the occupied
slots are clear.
If another availability or geometry change arrives during an entrance, the
group keeps the entrant's current interpolated position instead of restarting
it offscreen.
The group also requests frames for the short layout animation, so the
transition is visible under idle throttling; newly entering pill hit targets are
disabled until they reach their clear slots. The worktree, recent-folder, and
pull-request pills are unavailable while the focused terminal has a foreground
process, because their actions send commands to that shell. Their event
handlers reject input at the same boundary, before the scheduled update
collapses an already-open picker, so foreground-process input cannot be
consumed by stale UI. A busy or otherwise unavailable transition closes the
recent-folder, PR, and worktree pickers immediately, resetting their
transient state instead of exposing a partial collapse. It also clears any
pending worktree removal confirmation before the modal is hidden.
The group consumes pointer events over an entering pill so an overlapping
interactive pill underneath cannot receive the click. Pending Escape releases
remain consumed even if a busy transition closes the picker before key-up.

### External Notification Path

```
Expand Down Expand Up @@ -482,7 +511,8 @@ Rotate: rename active file to architect-<UTC timestamp>.log and continue in new
| `ui/components/modal_frame.zig` | Shared chrome for centered modal dialogs: full-window darkening scrim + rounded filled/bordered panel, and the Escape/⌘W dismiss-key check. Used by `confirm_dialog.zig` and `selection_agent_overlay.zig` so their scrim/panel rendering and dismissal keys can't drift independently | `renderScrimAndPanel()`, `isDismissKey()` | `gfx/primitives`, `geom`, `c` |
| `ui/components/dropdown_menu.zig` | Reusable vertical list menu: owns open/hover/keyboard-nav state and the committed `selected` index, renders its own cached item-label textures, and reports a `.selected`/`.closed` event on click or Enter/Escape so the owning component reacts (persist the pick, or act on it immediately) instead of tracking hit-testing and highlight rendering itself. Used by `selection_agent_overlay.zig`'s agent selector and `diff_overlay.zig`'s "Send to agent" menu | `DropdownMenu`, `openMenu()`, `close()`, `handleKey()`, `handleClick()`, `handleMotion()`, `itemAt()`, `itemRect()`, `render()` | `gfx/primitives`, `font_cache`, `ui/text_render`, `geom`, `c` |
| `ui/components/selection_agent_overlay.zig` | Selection action form with highlighted agent selector, multiline prompt field, fully wrapped and scrollable selected-context preview, viewport-bounded context textures, cached UI text, and launch action containing the selected terminal context | `SelectionAgentOverlayComponent`, `open()`, `formatAgentPrompt()` | `ui/text_edit`, `ui/text_render`, `ui/first_frame_guard`, `ui/components/modal_frame`, `ui/components/dropdown_menu`, `ui/components/scrollbar`, `gfx/primitives`, `font_cache`, `geom`, `c` |
| `ui/components/*` | Individual overlay and widget implementations conforming to `UiComponent` vtable. Includes: help overlay, worktree picker, recent folders picker (with instant search filtering), PR dropdown, diff viewer (with inline review comments), story viewer (PR story file visualization with rich markdown, anchor badges, bezier arrows, clickable links, and Cmd+F search — uses shared markdown parser/renderer pipeline and shared search utilities), reader mode overlay (uses shared search utilities), fullscreen overlay helper (shared animation/scroll/close logic embedded by story, diff, and reader overlays), reusable aqua-style scrollbar widget, session interaction, toast, quit confirm, quit-blocking overlay, restart buttons, escape hold indicator, metrics overlay, global shortcuts, pill group, cwd bar (its clipped parent-path marquee runs only in a focused Grid view), expanding overlay helper (badge-to-panel animation; `State.isOpenOrOpening()` is the canonical "this overlay owns the keyboard and is visible" test, so input is never dropped during the expand), button, confirm dialog (shares its scrim/panel chrome and dismiss-key check with the selection-agent overlay via `ui/components/modal_frame`), marquee label, hotkey indicator, flowing line, hold gesture detector. | Each component implements the `VTable` interface; overlays toggle via keyboard shortcuts or external commands and emit `UiAction` values. | `ui/component`, `ui/types`, `anim/easing`, `font`, `metrics`, `url_matcher`, `ui/session_view_state` |
| `ui/components/*` | Individual overlay and widget implementations conforming to `UiComponent` vtable. Includes: help overlay, worktree picker, recent folders picker (with instant search filtering), PR dropdown, diff viewer (with inline review comments), story viewer (PR story file visualization with rich markdown, anchor badges, bezier arrows, clickable links, and Cmd+F search — uses shared markdown parser/renderer pipeline and shared search utilities), reader mode overlay (uses shared search utilities), fullscreen overlay helper (shared animation/scroll/close logic embedded by story, diff, and reader overlays), reusable aqua-style scrollbar widget, session interaction, toast, quit confirm, quit-blocking overlay, restart buttons, escape hold indicator, metrics overlay, global shortcuts, dynamic pill group, cwd bar (its clipped parent-path marquee runs only in a focused Grid view), expanding overlay helper (badge-to-panel animation; `State.isOpenOrOpening()` is the canonical "this overlay owns the keyboard and is visible" test, so input is never dropped during the expand), button, confirm dialog (shares its scrim/panel chrome and dismiss-key check with the selection-agent overlay via `ui/components/modal_frame`), marquee label, hotkey indicator, flowing line, hold gesture detector. | Each component implements the `VTable` interface; overlays toggle via keyboard shortcuts or external commands and emit `UiAction` values. The pill group additionally owns right-aligned placement and eased membership transitions for the top-right shortcut row. | `ui/component`, `ui/types`, `anim/easing`, `font`, `metrics`, `url_matcher`, `ui/session_view_state` |
| `ui/components/pill_group.zig` | Dynamic top-right shortcut-pill layout and overlay coordination. Packs currently available pills with fixed spacing, animates existing pills when membership or window geometry changes, stages newly available pills during reflow, eases their entrance afterward, and synchronizes positions with overlay rendering and hit-testing. | `PillGroupComponent`, `PillLayout` | `ui/components/expanding_overlay`, `ui/components/help_overlay`, `ui/components/recent_folders_overlay`, `ui/components/worktree_overlay`, `ui/components/pr_dropdown`, `ui/first_frame_guard`, `anim/easing`, `dpi` |
| `ui/components/pr_dropdown.zig` | GitHub pull request picker orchestration: owns focused-repository state, input/lifecycle handling, repository-keyed worker jobs, stale-result filtering, branch badges, and checkout actions | `PRDropdownComponent` | `ui/components/pr_dropdown_model`, `ui/components/pr_dropdown_repo`, `ui/components/pr_dropdown_fetch`, `ui/components/pr_dropdown_view`, `ui/components/expanding_overlay`, `ui/components/search_utils`, `ui/text_edit`, `ui/types`, `geom`, `c` |
| `ui/components/pr_dropdown_model.zig` | Pull request and fetch result types plus pure repository/result matching predicates | `PullRequest`, `FetchStatus`, `FetchResult`, `freeFetchResult()`, `prNumberForBranch()` | std |
| `ui/components/pr_dropdown_repo.zig` | Synchronous repository discovery: `.git` and worktree config/HEAD resolution and GitHub origin detection | `findRepoRoot()`, `detectGithubOrigin()`, `readCurrentBranch()`, `originUrlIsGithub()` | std |
Expand Down
1 change: 1 addition & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ test {
_ = @import("ui/components/markdown_parser.zig");
_ = @import("ui/components/markdown_renderer.zig");
_ = @import("ui/components/metrics_overlay.zig");
_ = @import("ui/components/pill_group.zig");
_ = @import("ui/components/quit_blocking_overlay.zig");
_ = @import("ui/components/recent_folders_overlay.zig");
_ = @import("ui/components/pr_dropdown.zig");
Expand Down
42 changes: 39 additions & 3 deletions src/ui/components/expanding_overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub const ExpandingOverlay = struct {
start_size: c_int,
target_size: c_int,
slot: usize,
layout_x: ?c_int = null,
margin: c_int,
small_size: c_int,
large_size: c_int,
Expand Down Expand Up @@ -46,6 +47,12 @@ pub const ExpandingOverlay = struct {
self.content_height = height;
}

/// Sets the left edge of the collapsed pill. Expanded rectangles keep the
/// same right edge, so the panel grows to the left from this position.
pub fn setLayoutX(self: *ExpandingOverlay, x: c_int) void {
self.layout_x = x;
}

pub fn startExpanding(self: *ExpandingOverlay, now: i64) void {
self.state = .Expanding;
self.start_time = now;
Expand All @@ -64,6 +71,12 @@ pub const ExpandingOverlay = struct {
self.target_size = self.small_size;
}

pub fn closeImmediately(self: *ExpandingOverlay) void {
self.state = .Closed;
self.start_size = self.small_size;
self.target_size = self.small_size;
}

pub fn isAnimating(self: *const ExpandingOverlay) bool {
return self.state == .Expanding or self.state == .Collapsing;
}
Expand Down Expand Up @@ -91,17 +104,20 @@ pub const ExpandingOverlay = struct {
_ = window_height;
const margin = dpi.scale(self.margin, ui_scale);
const size = self.currentSize(now, ui_scale);
const small = dpi.scale(self.small_size, ui_scale);
const large = dpi.scale(self.large_size, ui_scale);
const spacing = dpi.scale(self.small_size + self.margin, ui_scale);
const x = window_width - margin - size - @as(c_int, @intCast(self.slot)) * spacing;
const x = if (self.layout_x) |collapsed_x|
collapsed_x - (size - small)
else
window_width - margin - size - @as(c_int, @intCast(self.slot)) * spacing;
const y = margin;

const height = blk: {
if (self.content_height == 0) {
break :blk size;
}

const small = dpi.scale(self.small_size, ui_scale);
const large = dpi.scale(self.large_size, ui_scale);
// content_height is already scaled by the overlay component
const target_height = self.content_height;

Expand Down Expand Up @@ -150,3 +166,23 @@ test "collapsing from the open state starts at full size" {
try std.testing.expectEqual(@as(c_int, 400), overlay.currentSize(1000, 1.0));
try std.testing.expectEqual(@as(c_int, 40), overlay.currentSize(1200, 1.0));
}

test "closeImmediately resets the overlay to its collapsed state" {
var overlay = ExpandingOverlay.init(0, 20, 40, 400, 200);
overlay.startExpanding(0);
overlay.closeImmediately();

try std.testing.expectEqual(ExpandingOverlay.State.Closed, overlay.state);
try std.testing.expectEqual(@as(c_int, 40), overlay.currentSize(100, 1.0));
try std.testing.expect(!overlay.isAnimating());
}

test "layout position keeps the expanded overlay right edge anchored" {
var overlay = ExpandingOverlay.init(0, 20, 40, 400, 200);
overlay.setLayoutX(600);
overlay.startExpanding(0);

const rect = overlay.rect(100, 800, 800, 1.0);
try std.testing.expectEqual(@as(c_int, 420), rect.x);
try std.testing.expectEqual(@as(c_int, 640), rect.x + rect.w);
}
Loading