Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Architect solves this with a grid view that keeps all your agents visible, with
- **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), 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 shows the current branch's PR number (e.g. `#123`); expand it to list open PRs (via `gh pr list`) with search filtering and check out a branch with `gh pr checkout`
- **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
- **Story viewer** — run `architect story <filename>` to open a scrollable overlay that renders PR story files with prose text and diff-colored code blocks
- **MCP session spawning** — run `architect-mcp` from an MCP client to ask the running Architect app to create a terminal session in a requested working directory
Expand All @@ -42,7 +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)
- 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
- 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, ⌘P for pull requests (GitHub repos), ⌘D for repo-wide git diff (staged + unstaged + untracked), ⌘R for reader mode, ⌘/ for shortcuts; quit with ⌘Q or the window close button
- Git diff overlay title shows the repo root folder being diffed
- Per-cell cwd bar in grid view reserves space, and terminal dimensions track grid/full mode so content wraps inside the visible area
- Scrollback with trackpad/wheel support and an auto-hiding draggable scrollbar in terminal views
Expand Down
34 changes: 32 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,36 @@ session.pending_write buffer
PTY write() -> shell process stdin
```

### Pull Request Listing Path

```
Cmd+P pressed -> pr_dropdown component
| check .git/config -> origin URL contains "github.com"?
| check .git/HEAD -> current branch
v
PRDropdownComponent.openOverlay()
| startFetch() spawns worker std.Thread
v
Worker thread: gh pr list --state open --json number,title,headRefName
| parse JSON
| acquire fetch_mutex
| write FetchResult into pending slot
| release fetch_mutex
| atomic.store(fetch_done, true)
v
Main loop next frame:
| wantsFrame() returns true (fetch_done == true)
| update() observes fetch_done, calls collectFetchResult()
| joins worker thread, drains pending slot, populates PR list
v
Render overlay with PR titles + branch matching for current PR badge

On Enter / click:
| emit UiAction.CheckoutPullRequest { session, pr_number, branch }
v
runtime.zig dispatch: send `gh pr checkout <number>\n` to the focused shell
```

### External Notification Path

```
Expand Down Expand Up @@ -418,7 +448,7 @@ Rotate: rename active file to architect-<UTC timestamp>.log and continue in new
| `ui/components/markdown_renderer.zig` | Line layout engine that wraps parsed markdown blocks into renderable lines and style runs, including prompt-separator and story-specific line kinds (diff headers, diff lines, code lines with anchor/kind metadata) | `buildLines()`, `freeLines()`, `RenderLine`, `RenderRun` | `ui/components/markdown_parser` |
| `ui/components/search_utils.zig` | Shared search utilities for overlays: case-insensitive substring find, match rebuilding, search bar rendering, and text texture creation | `SearchMatch`, `TextTex`, `findCaseInsensitive()`, `rebuildMatches()`, `renderSearchBar()`, `makeTextTexture()` | `gfx/primitives`, `font_cache`, `dpi`, `geom`, `c` |
| `ui/components/reader_overlay.zig` | Fullscreen reader overlay for the selected terminal history (full view or grid selection) with live markdown updates, centered reading-width layout, bottom pinning, jump-to-bottom, incremental search, clickable links, shared scrollbar interactions, styled inline markdown in table cells, and left-to-right gradient prompt separators | `ReaderOverlayComponent`, `toggle()` | `ui/components/fullscreen_overlay`, `ui/components/scrollbar`, `ui/components/search_utils`, `app/terminal_history`, `ui/components/markdown_parser`, `ui/components/markdown_renderer`, `os/open`, `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), 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, expanding overlay helper, button, confirm dialog, 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 (GitHub PR list fed by `gh pr list` on a background thread, current-PR detection via `.git/HEAD` + origin URL parsing), 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, expanding overlay helper, button, confirm dialog, 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` |
| `logging.zig` | File-backed structured logger with runtime level filtering and size-based rotation | `init()`, `deinit()`, `logFn()`, `writeEvent()`, `writeStartupMarker()`, `writeShutdownMarker()` | std |
| Shared Utilities (`geom`, `colors`, `dpi`, `config`, `logging`, `metrics`, `url_matcher`, `os/open`, `anim/easing`) | Geometry primitives, theme/palette management, DPI scaling helpers, TOML config loading/persistence, file-backed logging, performance metrics, URL detection, cross-platform URL opening, easing functions | `Rect`, `Theme`, `Config`, `logFn`, `Metrics`, `dpi.scale()`, `matchUrl()`, `open()`, `easeInOutCubic()`, `easeOutCubic()` | std, zig-toml, `c` |

Expand Down Expand Up @@ -538,7 +568,7 @@ Rotate: rename active file to architect-<UTC timestamp>.log and continue in new

- **Decision:** UI overlay components may perform synchronous I/O on the main thread for two categories of operations: (1) running short-lived `git` commands (e.g., `git diff`, `git rev-parse`) whose output is needed immediately for rendering, and (2) reading/writing small per-repo data files (e.g., `<repo>/.architect/diff_comments.json`).
- **Context:** The diff overlay needs `git diff` output to render its content and persists inline review comments as a small JSON file. ADR-009 establishes that blocking I/O should go on a background thread, but these operations complete in single-digit milliseconds for typical repositories and small data files. Introducing a background thread with a callback-based rendering pipeline for each git command would add significant complexity (deferred rendering, loading states, race conditions with overlay visibility) for negligible latency improvement.
- **Constraints:** This exception applies only when the data is small and the command is fast. Large or potentially slow operations (e.g., network I/O, cloning, `git log` on deep histories) must still use the background thread pattern from ADR-009.
- **Constraints:** This exception applies only when the data is small and the command is fast. Large or potentially slow operations (e.g., network I/O, cloning, `git log` on deep histories, `gh pr list` which hits the network) must still use the background thread pattern from ADR-009. The PR dropdown follows this rule: detection of a GitHub origin and the current branch is a synchronous read of `.git/config` and `.git/HEAD` on the main thread, but the actual `gh pr list` invocation runs on a worker thread spawned per-open with results delivered through a mutex-guarded slot and atomic completion flag.
- **Alternatives considered:**
- *Background thread + queue for all git commands* -- deferred; would require deferred rendering with loading states in the overlay, adding complexity disproportionate to the latency risk. May be revisited if git operations become noticeably slow on large repositories.
- *Lazy/cached persistence* -- partially adopted; comments are only saved on overlay close and on comment submit, not on every keystroke.
Expand Down
45 changes: 44 additions & 1 deletion src/app/runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1520,7 +1520,16 @@ pub fn run() !void {
};
try ui.register(help_component);

const pill_group_component = try ui_mod.pill_group.PillGroupComponent.create(allocator, help_comp_ptr, recent_folders_comp_ptr, worktree_comp_ptr);
const pr_dropdown_comp_ptr = try allocator.create(ui_mod.pr_dropdown.PRDropdownComponent);
pr_dropdown_comp_ptr.* = .{ .allocator = allocator };
const pr_dropdown_component = ui_mod.UiComponent{
.ptr = pr_dropdown_comp_ptr,
.vtable = &ui_mod.pr_dropdown.PRDropdownComponent.vtable,
.z_index = 1000,
};
try ui.register(pr_dropdown_component);

const pill_group_component = try ui_mod.pill_group.PillGroupComponent.create(allocator, help_comp_ptr, recent_folders_comp_ptr, worktree_comp_ptr, pr_dropdown_comp_ptr);
try ui.register(pill_group_component);
const toast_component = try ui_mod.toast.ToastComponent.init(allocator);
try ui.register(toast_component.asComponent());
Expand Down Expand Up @@ -2922,6 +2931,40 @@ pub fn run() !void {
}
allocator.free(story_action.path);
},
.CheckoutPullRequest => |pr_action| {
defer allocator.free(pr_action.branch);
if (pr_action.session >= sessions.len) continue;

var session = sessions[pr_action.session];
if (session.hasForegroundProcess()) {
ui.showToast("Stop the running process first", now);
continue;
}
if (!session.spawned or session.dead) {
ui.showToast("Start the shell first", now);
continue;
}

// Send `gh pr checkout <number>` to the focused shell. We rely on
// gh to do the heavy lifting (fetch, branch creation, switch).
var command_buf: [64]u8 = undefined;
const command = std.fmt.bufPrint(&command_buf, "\x15gh pr checkout {d}\n", .{pr_action.pr_number}) catch {
ui.showToast("Could not check out PR", now);
continue;
};
session.sendInput(command) catch |err| {
log.warn("failed to send pr checkout command: {}", .{err});
ui.showToast("Could not check out PR", now);
continue;
};

session_interaction_component.setStatus(pr_action.session, .running);
session_interaction_component.setAttention(pr_action.session, false, now);

var toast_buf: [64]u8 = undefined;
const toast_msg = std.fmt.bufPrint(&toast_buf, "Checking out PR #{d}…", .{pr_action.pr_number}) catch "Checking out PR…";
ui.showToast(toast_msg, now);
},
};

if (anim_state.mode == .Expanding or anim_state.mode == .Collapsing or
Expand Down
1 change: 1 addition & 0 deletions src/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub const SDLK_K = c_import.SDLK_K;
pub const SDLK_M = c_import.SDLK_M;
pub const SDLK_N = c_import.SDLK_N;
pub const SDLK_O = c_import.SDLK_O;
pub const SDLK_P = c_import.SDLK_P;
pub const SDLK_R = c_import.SDLK_R;
pub const SDLK_T = c_import.SDLK_T;
pub const SDLK_V = c_import.SDLK_V;
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/help_overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const shortcuts = [_]Shortcut{
.{ .key = "⌘↵", .desc = "Expand focused terminal" },
.{ .key = "⌘T", .desc = "Open worktree picker" },
.{ .key = "⌘O", .desc = "Open recent folders" },
.{ .key = "⌘P", .desc = "Open pull requests" },
.{ .key = "⌘?", .desc = "Open help" },
.{ .key = "⌘N", .desc = "Spawn new terminal" },
.{ .key = "⌘⇧+ / ⌘⇧-", .desc = "Adjust font size" },
Expand Down
29 changes: 29 additions & 0 deletions src/ui/components/pill_group.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const UiComponent = @import("../component.zig").UiComponent;
const HelpOverlayComponent = @import("help_overlay.zig").HelpOverlayComponent;
const WorktreeOverlayComponent = @import("worktree_overlay.zig").WorktreeOverlayComponent;
const RecentFoldersOverlayComponent = @import("recent_folders_overlay.zig").RecentFoldersOverlayComponent;
const PRDropdownComponent = @import("pr_dropdown.zig").PRDropdownComponent;

const ExpandingOverlay = @import("expanding_overlay.zig").ExpandingOverlay;

Expand All @@ -13,22 +14,26 @@ pub const PillGroupComponent = struct {
help: *HelpOverlayComponent,
recent_folders: *RecentFoldersOverlayComponent,
worktree: *WorktreeOverlayComponent,
pr_dropdown: *PRDropdownComponent,
last_help_state: ExpandingOverlay.State = .Closed,
last_recent_folders_state: ExpandingOverlay.State = .Closed,
last_worktree_state: ExpandingOverlay.State = .Closed,
last_pr_state: ExpandingOverlay.State = .Closed,

pub fn create(
allocator: std.mem.Allocator,
help: *HelpOverlayComponent,
recent_folders: *RecentFoldersOverlayComponent,
worktree: *WorktreeOverlayComponent,
pr_dropdown: *PRDropdownComponent,
) !UiComponent {
const comp = try allocator.create(PillGroupComponent);
comp.* = .{
.allocator = allocator,
.help = help,
.recent_folders = recent_folders,
.worktree = worktree,
.pr_dropdown = pr_dropdown,
};

return UiComponent{
Expand Down Expand Up @@ -57,10 +62,12 @@ pub const PillGroupComponent = struct {
const help_state = self.help.overlay.state;
const recent_folders_state = self.recent_folders.overlay.state;
const worktree_state = self.worktree.overlay.state;
const pr_state = self.pr_dropdown.overlay.state;

const help_started_expanding = self.last_help_state != .Expanding and help_state == .Expanding;
const recent_folders_started_expanding = self.last_recent_folders_state != .Expanding and recent_folders_state == .Expanding;
const worktree_started_expanding = self.last_worktree_state != .Expanding and worktree_state == .Expanding;
const pr_started_expanding = self.last_pr_state != .Expanding and pr_state == .Expanding;

// When one overlay starts expanding, collapse the others
if (help_started_expanding) {
Expand All @@ -70,6 +77,9 @@ pub const PillGroupComponent = struct {
if (worktree_state == .Open or worktree_state == .Expanding) {
self.worktree.overlay.startCollapsing(host.now_ms);
}
if (pr_state == .Open or pr_state == .Expanding) {
self.pr_dropdown.overlay.startCollapsing(host.now_ms);
}
}

if (recent_folders_started_expanding) {
Expand All @@ -79,6 +89,9 @@ pub const PillGroupComponent = struct {
if (worktree_state == .Open or worktree_state == .Expanding) {
self.worktree.overlay.startCollapsing(host.now_ms);
}
if (pr_state == .Open or pr_state == .Expanding) {
self.pr_dropdown.overlay.startCollapsing(host.now_ms);
}
}

if (worktree_started_expanding) {
Expand All @@ -88,11 +101,27 @@ pub const PillGroupComponent = struct {
if (recent_folders_state == .Open or recent_folders_state == .Expanding) {
self.recent_folders.overlay.startCollapsing(host.now_ms);
}
if (pr_state == .Open or pr_state == .Expanding) {
self.pr_dropdown.overlay.startCollapsing(host.now_ms);
}
}

if (pr_started_expanding) {
if (help_state == .Open or help_state == .Expanding) {
self.help.overlay.startCollapsing(host.now_ms);
}
if (recent_folders_state == .Open or recent_folders_state == .Expanding) {
self.recent_folders.overlay.startCollapsing(host.now_ms);
}
if (worktree_state == .Open or worktree_state == .Expanding) {
self.worktree.overlay.startCollapsing(host.now_ms);
}
}

self.last_help_state = help_state;
self.last_recent_folders_state = recent_folders_state;
self.last_worktree_state = worktree_state;
self.last_pr_state = pr_state;
}

fn render(_: *anyopaque, _: *const types.UiHost, _: *c.SDL_Renderer, _: *types.UiAssets) void {}
Expand Down
Loading
Loading