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
8 changes: 5 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,9 @@ Status notifications -> SessionStatus updated (idle -> awaiting_approval)
Story notifications -> StoryOverlay opens with file content
|
v
Renderer draws attention border / story overlay
Renderer draws terminal content and borders; the cwd-bar UI component reasserts
the focused border after its label so attention tiles keep the same focus-border
z-order as ordinary tiles.
```

### External MCP Spawn Path
Expand Down Expand Up @@ -493,13 +495,13 @@ Rotate: rename active file to architect-<UTC timestamp>.log and continue in new
| `session/pty_reader.zig` | Background thread that `poll(2)`s spawned sessions' PTY master fds and drains readable ones into per-session SPSC ring buffers; registry with retire handshake so teardown can safely close fds | `PtyReader`, `PtyOutputBuffer`, `start()`, `register()`, `retire()` | std (poll, thread) |
| `wake_pipe.zig` | Non-blocking self-pipe used to wake blocking background-thread polls for PTY registry changes and shutdown | `WakePipe`, `poll_error_backoff_ns` | `posix_util`, std (poll) |
| `session/*` (shell, pty, vt_stream, cwd) | Shell spawning, PTY abstraction, VT parsing, working directory detection | `spawn()`, `Pty`, `VtStream.processBytes()`, `getCwd()` | std (posix), ghostty-vt |
| `render/renderer.zig` | Scene rendering: terminals, borders, animations, terminal scrollbar painting, first-launch onboarding hint | `render()`, `RenderCache`, per-session texture management | `font`, `font_cache`, `gfx/*`, `anim/easing`, `app/app_state`, `ui/components/scrollbar`, `c` |
| `render/renderer.zig` | Scene rendering: terminals, borders, animations, terminal scrollbar painting, first-launch onboarding hint. Exposes the shared focused-border primitive used by the cwd-bar UI component to preserve border z-order after the bar label | `render()`, `RenderCache`, `renderFocusBorder()`, per-session texture management | `font`, `font_cache`, `gfx/*`, `anim/easing`, `app/app_state`, `ui/components/scrollbar`, `c` |
| `font.zig` + `font_cache.zig` | Font rendering, HarfBuzz shaping, glyph LRU cache, shared font cache | `Font`, `openFont()`, `renderGlyph()`, `FontCache`, `getOrCreate()` | `font_paths`, `c` (SDL3_ttf) |
| `gfx/*` (box_drawing, primitives) | Procedural box-drawing characters (U+2500-U+257F), rounded/thick border helpers, bezier arrow rendering | `renderBoxDrawing()`, `drawRoundedRect()`, `drawThickBorder()`, `fillRoundedRect()`, `renderBezierArrow()` | `c` |
| `env.zig`, `clock.zig`, `proc.zig` | Process-environment access, I/O-aware timestamps/sleep, and I/O-aware process execution helpers | `get()`, `now*()`, `sleepNanos()`, `run()`, `spawnDetached()` | std |
| `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/types.zig` | Shared UI type definitions, including per-session attention state needed by grid chrome | `UiHost`, `UiAction`, `UiActionQueue`, `UiAssets`, `SessionUiInfo` | `app/app_state`, `colors`, `font`, `geom` |
| `ui/session_view_state.zig` | Per-session UI interaction state (selection, scroll, hover, agent status, scrollbar fade/drag state) | `SessionViewState` (selection, scroll offset, hover, status, terminal scrollbar state) | `app/app_state` (for `SessionStatus` enum), `ui/components/scrollbar` |
| `ui/first_frame_guard.zig` | Idle throttle bypass for visible state transitions | `FirstFrameGuard`, `markTransition()`, `markDrawn()`, `wantsFrame()` | (none) |
| `ui/text_edit.zig` | Shared text-field model used by every input (worktree name, recent-folder/reader/story search, diff comments). `TextInput` owns the buffer, caret blink phase and select-all flag, and handles Backspace (⌘ clears / ⌥ word / plain one UTF-8 codepoint), ⌘A, ⌘C and ⌘V. Append-only by design: the caret sits at the end and the only selection is "everything". Components keep layout and rendering, reading `caretVisible()`/`select_all` for the visuals. | `TextInput`, `handleKey()`, `insert()`, `caretVisible()`, `touch()`, `DeleteScope`, `scopeFromMods()`, `backspace()`, `isSingleLineChar()`, `name_separators`, `path_separators`, `prose_separators` | `c` (keycodes, clipboard) |
Expand Down
1 change: 1 addition & 0 deletions src/app/ui_host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub fn makeUiHost(
buffer[i] = .{
.dead = session.dead,
.spawned = session.spawned,
.attention = if (i < views.len) views[i].attention else false,
.cwd_path = session.cwd_path,
.cwd_basename = session.cwd_basename,
.session_status = if (i < views.len) views[i].status else .idle,
Expand Down
73 changes: 51 additions & 22 deletions src/render/renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,56 @@ test "grid active screen rendering follows cursor row" {
try std.testing.expectEqual(@as(usize, 0), activeScreenRowOffset(50, 0, 49, true, false));
}

/// Return the geometry of a focused border, including the attention inset.
fn focusBorderRect(rect: Rect, has_attention: bool, border_thickness: c_int) ?Rect {
const inset: c_int = if (has_attention) border_thickness else 0;
var focus_rect = rect;
focus_rect.x += inset;
focus_rect.y += inset;
focus_rect.w -= inset * 2;
focus_rect.h -= inset * 2;
if (focus_rect.w <= 0 or focus_rect.h <= 0) return null;
return focus_rect;
}

/// Draw the focused grid border. Attention borders inset the focus border, so
/// the cwd bar redraws this after its label to preserve the intended z-order.
pub fn renderFocusBorder(
renderer: *c.SDL_Renderer,
rect: Rect,
has_attention: bool,
theme: *const colors.Theme,
ui_scale: f32,
draw_fill: bool,
) void {
const focus_blue = theme.palette[12];
const border_thickness: c_int = dpi.scale(attention_thickness, ui_scale);
const focus_rect = focusBorderRect(rect, has_attention, border_thickness) orelse return;

_ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_BLEND);
if (draw_fill and !has_attention) {
_ = c.SDL_SetRenderDrawColor(renderer, focus_blue.r, focus_blue.g, focus_blue.b, 38);
_ = c.SDL_RenderFillRect(renderer, &c.SDL_FRect{
.x = @floatFromInt(focus_rect.x),
.y = @floatFromInt(focus_rect.y),
.w = @floatFromInt(focus_rect.w),
.h = @floatFromInt(focus_rect.h),
});
}
primitives.drawThickBorder(renderer, focus_rect, border_thickness, dpi.scale(6, ui_scale), focus_blue);
}

test "focused border is inset only for attention tiles" {
const rect = Rect{ .x = 10, .y = 20, .w = 100, .h = 80 };

try std.testing.expectEqual(rect, focusBorderRect(rect, false, 6).?);
try std.testing.expectEqual(
Rect{ .x = 16, .y = 26, .w = 88, .h = 68 },
focusBorderRect(rect, true, 6).?,
);
try std.testing.expectEqual(@as(?Rect, null), focusBorderRect(Rect{ .x = 0, .y = 0, .w = 12, .h = 12 }, true, 6));
}

fn renderSessionOverlays(
renderer: *c.SDL_Renderer,
session: *SessionState,
Expand Down Expand Up @@ -1038,28 +1088,7 @@ fn renderSessionOverlays(
primitives.drawThickBorder(renderer, rect, border_thickness, border_radius, base_border);
}

if (is_focused) {
const focus_blue = theme.palette[12];
const inset: c_int = if (has_attention) border_thickness else 0;
var focus_rect = rect;
focus_rect.x += inset;
focus_rect.y += inset;
focus_rect.w -= inset * 2;
focus_rect.h -= inset * 2;
if (focus_rect.w > 0 and focus_rect.h > 0) {
_ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_BLEND);
if (!has_attention) {
_ = c.SDL_SetRenderDrawColor(renderer, focus_blue.r, focus_blue.g, focus_blue.b, 38);
_ = c.SDL_RenderFillRect(renderer, &c.SDL_FRect{
.x = @floatFromInt(focus_rect.x),
.y = @floatFromInt(focus_rect.y),
.w = @floatFromInt(focus_rect.w),
.h = @floatFromInt(focus_rect.h),
});
}
primitives.drawThickBorder(renderer, focus_rect, border_thickness, border_radius, focus_blue);
}
}
if (is_focused) renderFocusBorder(renderer, rect, has_attention, theme, ui_scale, true);
}

if (has_attention) {
Expand Down
3 changes: 3 additions & 0 deletions src/ui/components/cwd_bar.zig
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ pub const CwdBarComponent = struct {
.h = host.cell_h,
};
self.renderCwdBar(renderer, i, info, cell_rect, host, cache, i);
if (i == host.focused_session) {
renderer_mod.renderFocusBorder(renderer, cell_rect, info.attention, host.theme, host.ui_scale, false);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/ui/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const geom = @import("../geom.zig");
pub const SessionUiInfo = struct {
dead: bool,
spawned: bool,
attention: bool = false,
cwd_path: ?[]const u8 = null,
cwd_basename: ?[]const u8 = null,
session_status: app_state.SessionStatus = .idle,
Expand Down