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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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
- **Recent folders** (⌘O) — quickly `cd` into recently visited directories with arrow key selection
- **Diff review comments** — click diff lines in the ⌘D overlay to leave inline comments, then send them all to a running agent (or start one) with the "Send to agent" button

### Terminal Essentials
- Smooth animated transitions for grid expansion, contraction, and reflow (cells and borders move/resize together)
Expand Down
19 changes: 15 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ Renderer draws attention border (pulsing yellow / solid green)
| Render cache | GPU textures per session | Cached terminal renders, epoch-invalidated |
| config.toml | `~/.config/architect/config.toml` | User preferences (font, theme, UI flags) |
| persistence.toml | `~/.config/architect/persistence.toml` | Runtime state (window pos, font size, terminal cwds) |
| diff_comments.json | `<repo>/.architect/diff_comments.json` | Per-repo inline diff review comments (unsent) |

### Exit Points

Expand All @@ -280,7 +281,7 @@ Renderer draws attention border (pulsing yellow / solid green)
|--------|---------------|----------------------------------|--------------|
| `main.zig` | Thin entrypoint | `main()` | `app/runtime` |
| `app/runtime.zig` | Application lifetime, frame loop, session spawning, config persistence | `run()`, frame loop internals | `platform/sdl`, `session/state`, `render/renderer`, `ui/root`, `config`, all `app/*` modules |
| `app/*` (app_state, layout, ui_host, grid_nav, grid_layout, input_keys, input_text, terminal_actions, worktree) | Application logic decomposed by concern: state enums, grid sizing, UI snapshot building, navigation, input encoding, clipboard, worktree commands | `ViewMode`, `AnimationState`, `SessionStatus`, `buildUiHost()`, `applyTerminalResize()`, `encodeKey()`, `paste()`, `clear()` | `geom`, `anim/easing`, `ui/types`, `colors`, `input/mapper`, `session/state`, `c` |
| `app/*` (app_state, layout, ui_host, grid_nav, grid_layout, input_keys, input_text, terminal_actions, worktree) | Application logic decomposed by concern: state enums, grid sizing, UI snapshot building, navigation, input encoding, clipboard, worktree commands | `ViewMode`, `AnimationState`, `SessionStatus`, `buildUiHost()`, `applyTerminalResize()`, `encodeKey()`, `paste()`, `clear()` | `geom`, `anim/easing`, `ui/types`, `ui/session_view_state`, `colors`, `input/mapper`, `session/state`, `c` |
| `platform/sdl.zig` | SDL3 initialization, window management, HiDPI | `init()`, `createWindow()`, `createRenderer()` | `c` |
| `input/mapper.zig` | SDL keycodes to VT escape sequences, shortcut detection | `encodeKey()`, modifier helpers | `c` |
| `c.zig` | C FFI re-exports (SDL3, SDL3_ttf constants) | `SDLK_*`, `SDL_*`, `TTF_*` re-exports | SDL3 system libs (via `@cImport`) |
Expand All @@ -293,10 +294,10 @@ Renderer draws attention border (pulsing yellow / solid green)
| `ui/root.zig` | UI component registry, z-index dispatch, action drain | `UiRoot`, `register()`, `handleEvent()`, `update()`, `render()`, `needsFrame()` | `ui/component`, `ui/types` |
| `ui/component.zig` | UI component vtable interface | `UiComponent`, `VTable` (handleEvent, update, render, hitTest, wantsFrame, deinit) | `ui/types`, `c` |
| `ui/types.zig` | Shared UI type definitions | `UiHost`, `UiAction`, `UiActionQueue`, `UiAssets`, `SessionUiInfo` | `app/app_state`, `colors`, `font`, `geom` |
| `ui/session_view_state.zig` | Per-session UI interaction state | `SessionViewState` (selection, scroll offset, hover) | (none) |
| `ui/session_view_state.zig` | Per-session UI interaction state (selection, scroll, hover, agent status) | `SessionViewState` (selection, scroll offset, hover, status) | `app/app_state` (for `SessionStatus` enum) |
| `ui/first_frame_guard.zig` | Idle throttle bypass for visible state transitions | `FirstFrameGuard`, `markTransition()`, `markDrawn()`, `wantsFrame()` | (none) |
| `ui/components/*` | Individual overlay and widget implementations conforming to `UiComponent` vtable. Includes: help overlay, worktree picker, recent folders picker, diff viewer, session interaction, toast, quit confirm, 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 and emit `UiAction` values. | `ui/component`, `ui/types`, `anim/easing`, `font`, `metrics`, `url_matcher`, `ui/session_view_state` |
| Shared Utilities (`geom`, `colors`, `config`, `metrics`, `url_matcher`, `os/open`, `anim/easing`) | Geometry primitives, theme/palette management, TOML config loading/persistence, performance metrics, URL detection, cross-platform URL opening, easing functions | `Rect`, `Theme`, `Config`, `Metrics`, `matchUrl()`, `open()`, `easeInOutCubic()` | std, zig-toml, `c` |
| `ui/components/*` | Individual overlay and widget implementations conforming to `UiComponent` vtable. Includes: help overlay, worktree picker, recent folders picker, diff viewer (with inline review comments), session interaction, toast, quit confirm, 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 and emit `UiAction` values. | `ui/component`, `ui/types`, `anim/easing`, `font`, `metrics`, `url_matcher`, `ui/session_view_state` |
| Shared Utilities (`geom`, `colors`, `config`, `metrics`, `url_matcher`, `os/open`, `anim/easing`) | Geometry primitives, theme/palette management, TOML config loading/persistence, performance metrics, URL detection, cross-platform URL opening, easing functions | `Rect`, `Theme`, `Config`, `Metrics`, `matchUrl()`, `open()`, `easeInOutCubic()`, `easeOutCubic()` | std, zig-toml, `c` |

## Key Architectural Decisions

Expand Down Expand Up @@ -409,3 +410,13 @@ Renderer draws attention border (pulsing yellow / solid green)
- *Always render at full rate* -- rejected because it wastes CPU/GPU when nothing is changing, impacting battery life on laptops.
- *SDL event injection* -- rejected because synthetic events pollute the event queue and complicate event handling logic.
- **Date:** 2025 (UI system refinement)

### ADR-013: Synchronous I/O in UI Overlays for Git and Small-File Persistence

- **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.
- **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.
- **Date:** 2025 (diff overlay inline comments)
5 changes: 5 additions & 0 deletions src/anim/easing.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ pub fn easeInOutCubic(t: f32) f32 {
const p = 2 * t - 2;
return 1 + p * p * p / 2;
}

pub fn easeOutCubic(t: f32) f32 {
const p = t - 1.0;
return 1.0 + p * p * p;
}
56 changes: 56 additions & 0 deletions src/app/runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ fn highestSpawnedIndex(sessions: []const *SessionState) ?usize {
return null;
}

fn agentProcessStarted(session: *const SessionState) bool {
return session.hasForegroundProcess();
}

fn adjustedRenderHeightForMode(mode: app_state.ViewMode, render_height: c_int, ui_scale: f32, grid_rows: usize) c_int {
return switch (mode) {
.Grid, .Expanding, .Collapsing, .GridResizing => blk: {
Expand Down Expand Up @@ -579,6 +583,13 @@ pub fn run() !void {
var relaunch_trace_frames: u8 = 0;
var window_close_suppress_countdown: u8 = 0;

const PendingCommentSend = struct {
session: usize,
text: []const u8,
send_after_ms: i64,
};
var pending_comment_send: ?PendingCommentSend = null;

const session_interaction_component = try ui_mod.SessionInteractionComponent.init(allocator, sessions, &font);
try ui.register(session_interaction_component.asComponent());

Expand Down Expand Up @@ -689,6 +700,7 @@ pub fn run() !void {
&anim_state,
sessions,
session_ui_info,
session_interaction_component.viewSlice(),
focused_has_foreground_process,
&theme,
);
Expand Down Expand Up @@ -1410,6 +1422,20 @@ pub fn run() !void {
std.debug.print("Session {d} (slot {d}) status -> {s}\n", .{ note.session, session_idx, @tagName(note.state) });
}

if (pending_comment_send) |pcs| {
const prompt_ready = pcs.session < sessions.len and
agentProcessStarted(sessions[pcs.session]);
if (now >= pcs.send_after_ms or prompt_ready) {
if (pcs.session < sessions.len) {
sessions[pcs.session].sendInput(pcs.text) catch |err| {
log.warn("failed to send pending diff comments: {}", .{err});
};
}
allocator.free(pcs.text);
pending_comment_send = null;
}
}

var focused_has_foreground_process = foreground_cache.get(now, anim_state.focused_session, sessions);
const ui_update_host = ui_host.makeUiHost(
now,
Expand All @@ -1425,6 +1451,7 @@ pub fn run() !void {
&anim_state,
sessions,
session_ui_info,
session_interaction_component.viewSlice(),
focused_has_foreground_process,
&theme,
);
Expand Down Expand Up @@ -1832,6 +1859,34 @@ pub fn run() !void {
.opened => if (config.ui.show_hotkey_feedback) ui.showHotkey("⌘D", now),
}
},
.SendDiffComments => |dc_action| {
if (dc_action.session >= sessions.len) {
allocator.free(dc_action.comments_text);
if (dc_action.agent_command) |cmd| allocator.free(cmd);
continue;
}
var dc_session = sessions[dc_action.session];
if (dc_action.agent_command) |cmd| {
dc_session.sendInput(cmd) catch |err| {
log.warn("failed to send agent command: {}", .{err});
allocator.free(dc_action.comments_text);
allocator.free(cmd);
continue;
};
allocator.free(cmd);
if (pending_comment_send) |prev| allocator.free(prev.text);
pending_comment_send = .{
.session = dc_action.session,
.text = dc_action.comments_text,
.send_after_ms = now + 2000,
};
} else {
dc_session.sendInput(dc_action.comments_text) catch |err| {
log.warn("failed to send diff comments: {}", .{err});
};
allocator.free(dc_action.comments_text);
}
},
};

if (anim_state.mode == .Expanding or anim_state.mode == .Collapsing or
Expand Down Expand Up @@ -1896,6 +1951,7 @@ pub fn run() !void {
&anim_state,
sessions,
session_ui_info,
session_interaction_component.viewSlice(),
focused_has_foreground_process,
&theme,
);
Expand Down
4 changes: 4 additions & 0 deletions src/app/ui_host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ const c = @import("../c.zig");
const colors_mod = @import("../colors.zig");
const session_state = @import("../session/state.zig");
const ui_mod = @import("../ui/mod.zig");
const view_state = @import("../ui/session_view_state.zig");

const AnimationState = app_state.AnimationState;
const Rect = app_state.Rect;
const SessionState = session_state.SessionState;
const SessionViewState = view_state.SessionViewState;

pub fn applyMouseContext(ui: *ui_mod.UiRoot, host: *ui_mod.UiHost, event: *const c.SDL_Event) void {
switch (event.type) {
Expand Down Expand Up @@ -60,6 +62,7 @@ pub fn makeUiHost(
anim_state: *const AnimationState,
sessions: []const *SessionState,
buffer: []ui_mod.SessionUiInfo,
views: []const SessionViewState,
focused_has_foreground_process: bool,
theme: *const colors_mod.Theme,
) ui_mod.UiHost {
Expand All @@ -69,6 +72,7 @@ pub fn makeUiHost(
.spawned = session.spawned,
.cwd_path = session.cwd_path,
.cwd_basename = session.cwd_basename,
.session_status = if (i < views.len) views[i].status else .idle,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ else
0x0000_0004;
pub const SDL_SetRenderDrawBlendMode = c_import.SDL_SetRenderDrawBlendMode;
pub const SDL_SetRenderClipRect = c_import.SDL_SetRenderClipRect;
pub const SDL_GetRenderClipRect = c_import.SDL_GetRenderClipRect;
pub const SDL_GetTextureSize = c_import.SDL_GetTextureSize;
pub const SDL_CreateTextureFromSurface = c_import.SDL_CreateTextureFromSurface;
pub const SDL_DestroyTexture = c_import.SDL_DestroyTexture;
Expand Down
Loading