chore: better ui/ux#1
Conversation
Removes the macOS provisioning engine that lived alongside the diff/review
HTTP API in the same Go binary: workflow runner, app/brew/stow/macOS
installers, 1Password sync, snapshots, and the doctor/inventory pipeline.
- Deletes internal/{service,converge,currentstate,domain,template,snapshot},
internal/app/{workflows,cli_workflow,permissions}.go and the
workflows/runs/template-files/onepassword httpx routes.
- Removes service/Workflows wiring from httpx.Server and the
list-workflows/run-workflow CLI subcommands from app.go.
- Slims RuntimeSettings to {RepoRoot, DatabasePath} and renames the
--workflow-db flag to --db plus the env var to GIT_DIFF_DB_PATH.
- Strips workflow types and methods from internal/storage and rebuilds the
sqlc bindings against an empty query.sql (all queries live directly in
store.go now). Adds a one-shot DROP TABLE for workflow_runs/events so
existing user databases shed the orphaned tables on first launch.
- Drops the stow/ dotfile tree, apps.yaml, secrets.yaml, and the
workflow-only tests in app/httpx/storage.
Reflects the now-actual scope of the package: an HTTP API serving the
diff/review surface to the Electron UI, not a Mac provisioning engine.
- git mv packages/macbook packages/api (preserves history on every file)
- go.work: ./packages/macbook -> ./packages/api
- packages/api/package.json: name "macbook" -> "api"
- packages/ui/{electron,scripts/dev}/paths.ts: macbookDir -> apiDir
- packages/ui/electron/bridge.ts and dev/backend.ts, dev/watchers.ts:
consume apiDir for the dev spawn cwd
- packages/ui/package.json electron-builder extraResources from path:
../macbook/dist/api -> ../api/dist/api
- packages/tools/internal/release/release.go pnpm build path follows
- internal/app/repo_root.go: drop the legacy packages/macbook fallback
in walkForRepoRoot now that the binary lives under packages/api
- Drop the now-meaningless TestFindRepoRootFromOuterRepoUsesMacOSDir
- packages/tools/internal/turbocache/turbocache_test.go fixture renamed
The parsing path in src/lib/patch.ts already routes through @pierre/diffs' parsePatchFiles helper, so the only delta is keeping our version aligned with what upstream codiff uses. Adds packages/ui/AGENTS.md to lock in the Vue-only constraint: this package never adopts React or React-interop shims, even if a library ships a useful React component. @pierre/diffs' React <CodeView> stays off-limits; parsing helpers (parsePatchFiles, parseDiffFromFile, FileDiffMetadata) are framework-neutral and welcome.
A user can now review any historical commit through the same diff UI used
for working-tree changes. A 'History' picker in the chrome lists recent
commits; selecting one loads its diff. A 'Back to working tree' button
returns to the default mode. Comments persist scoped to the commit SHA via
new context columns on review_sessions.
Backend:
- ReadCommitState resolves the SHA via rev-parse --verify ^{commit}, lists
files via diff-tree --name-status, and assembles per-file patches via
git show --first-parent --format=. The shape matches ReadRepositoryState
with Mode set to "commit" and a new CommitSHA field.
- ListCommitLog returns the most recent commits for the picker as
CommitSummary{ SHA, ShortSHA, Author, Email, Date, Subject }.
- Routes: GET /v1/repository/commit?path=&sha= and GET /v1/repository/log
with the existing auth gate.
- review_sessions gets context_kind/context_sha columns. A one-shot
ALTER TABLE in Store.Init brings legacy databases up to the new schema.
CreateReview accepts ContextKind/ContextSHA and persists them.
- Two new tests cover ReadCommitState (Mode, sections, short SHA accepted,
bad SHA rejected) and ListCommitLog (ordering, author/email/date).
Bridge / IPC:
- readCommit, listCommits added to the typed client + the diffApp surface
exposed to the renderer.
- DiffSectionKind widens to include "commit"; RepositoryMode union added.
UI:
- New CommitPicker.vue popover with recent commits.
- App.vue tracks repoMode + commits, branches refresh between working and
commit modes, passes contextKind/contextSha when starting a review.
- Browser fallback gets a stub readCommit/listCommits so dev mode keeps
working without the Go bridge.
The milestone deliberately does not touch the diff renderer: a commit
diff flows through the same DiffBody.vue, just with section.kind="commit".
There was a problem hiding this comment.
Code Review
This pull request refactors the backend from a general machine management tool into a dedicated git-diff review API, renaming the package from macbook to api. The changes involve removing legacy workflow and provisioning logic, simplifying runtime settings, and introducing new functionality to browse repository history and inspect specific commits. The storage layer has been updated with schema migrations to support review sessions, and the UI now includes a commit picker component. Feedback identifies a security vulnerability in a deprecated dependency (@ungap/structured-clone), points out outdated error messages that should be updated to reflect the new database context, and recommends proper error handling for the limit parameter in the repository log endpoint.
git-diff now accepts a real CLI surface from the terminal helper: git-diff open the most recently used repo git-diff <path> open a specific repository git-diff <sha> open a specific commit from cwd git-diff -w [<sha>] request an AI walkthrough (planned) git-diff -h|--help print usage and exit Implementation: - electron/launch-intent.ts parses process.argv into a structured intent. An existing directory always beats the SHA pattern, so 'git-diff abc1234' in a repo with an abc1234/ directory opens the directory. - launch-intent-store.ts is a one-shot queue: the renderer drains it on enterApp(), and second-instance pushes refresh the queue plus emit a launch-intent:updated IPC for live update of the running window. - main.ts hosts the parser, surfaces help via stderr + a Cocoa dialog, and quits with code 0 on help. - ipc.ts exposes launch-intent:take; preload.ts wires window.diffApp.takeLaunchIntent and onLaunchIntent. - App.vue.applyLaunchIntent() runs after auth ready: it opens repoPath (or falls back to the last-repo pref), then calls openCommit when a SHA was supplied. The walkthrough flag is recorded but no-ops until Milestone 6. - 13 vitest cases in tests/launch-intent.test.ts cover: no args, paths (relative + absolute), SHA detection, directory-beats-SHA, -w flag in either order, --help, unknown flag, unresolvable arg, too many positionals, dev-mode argv prefix stripping, and Electron internal flag filtering. Browser fallback gets no-op takeLaunchIntent/onLaunchIntent so the dev shell still satisfies DiffAppApi.
Two `git-diff` invocations from two terminals now produce two side-by-side
windows on the same Electron process. Closing one doesn't affect the other.
Each window carries its own launch intent (repo path, optional commit SHA,
walkthrough flag) so per-window state stays isolated.
- windows.ts replaces the mainWindow singleton with a Map<webContentsId,
WindowEntry> keyed by webContents.id. Each entry holds the BrowserWindow,
the consumed-on-take launch intent, and the per-window devtools child.
Subsequent windows cascade by 32px so they don't overlap exactly.
- main.ts's second-instance handler parses the new argv and calls
createWindow(intent) instead of focusing the existing window. After the
renderer loads, it broadcasts launch-intent:updated for live update.
- app.on('activate') (macOS dock click) only creates a window if none are
open, matching standard macOS behavior.
- ipc.ts gains window:new for the File > New Window… menu and refactors
launch-intent:take to resolve the per-window entry via
BrowserWindow.fromWebContents(event.sender).
- New menu.ts installs a real application menu with File > New Window…
(Cmd+N), the standard editMenu/View/Window roles, and a macOS app menu.
The dialog opens a directory picker and spawns a fresh window pointed at
the chosen path.
- launch-intent-store.ts is gone — per-window state lives on the
WindowEntry directly, so it can't get crossed between windows.
- preload + DiffAppApi gain openNewWindow(repoPath?) for the renderer to
request additional windows. Browser fallback gets a no-op.
The Go backend doesn't change: every existing repo route already accepts a
?path= override, so two windows share one Go process and route their calls
by repo root.
A "Copy review as Markdown" button on the active-review chrome flattens the current review session into a Markdown document and writes it to the clipboard. Output groups comments by file, sorts within a file by line number then created_at, and includes a header with repo path + branch or commit SHA. - src/lib/reviewMarkdown.ts implements both formatReviewAsMarkdown and a dependency-free htmlToMarkdown converter scoped to the tag subset our Lexical rich-text editor emits (p, br, strong/b, em/i, code, pre, ul/ol/li, a, blockquote). Unknown tags collapse to their text content. Avoiding turndown keeps us off another DOM-coupled dep. - ReviewComment gains an optional deletedAt field so the soft-deleted comments the backend already tracks are now exposed and filtered out of the export. - App.vue gets a copyReviewState ref + button next to the commit-mode banner. The button is hidden until there is an active review; it flashes "Copied!" for 1.5s on success. - 10 vitest cases in tests/reviewMarkdown.test.ts cover paragraphs, inline vs block code, lists (ordered + unordered), links, blockquote, fallback to text on unknown tags, working-tree vs commit-mode headers, deleted comments hidden, and the empty-review "_No comments yet._" line. No backend change required.
Press n to jump to the next hunk, p to jump to the previous one. The target hunk header scrolls to the center of the viewport with a 350ms border-glow flash so the jump is visually confirmed. - DiffBody.vue tags each hunk-header div (split and unified mode) with data-hunk-anchor. - App.vue's existing onKeydown handler now also intercepts n/p (without modifier keys, not in inputs) and runs jumpToHunk, which queries every data-hunk-anchor in the document, finds the anchor closest to the midpoint of the viewport as the "current" one, and scrolls delta hunks away. - style.css gets a small @Keyframes gd-hunk-flash for the confirmation glow. The document-wide query keeps the wiring simple — no defineExpose hoop or per-DiffBody ref tracking, no matter how many files are rendered.
A "Generate walkthrough" button on the active-review chrome calls the
Anthropic Messages API with a structured prompt built from the current
RepositoryState (working tree or commit-mode), gets back an ordered list
of file paths plus a one-line note per file, and renders the result
inline. Subsequent calls hit a SQLite cache keyed by (repo_root,
context_kind, context_sha) and only round-trip to the API when the
file-set fingerprint changes (or the user clicks "Refresh walkthrough").
Backend:
- internal/walkthrough/walkthrough.go houses the API client. Generate()
builds the prompt (file paths, status, ± counts, truncated patches up
to ~60k chars total), calls /v1/messages, strips ```json fences from
the response, decodes the {order, notes, summary} JSON, and filters
out any hallucinated paths the model invents. FingerprintForState
produces a deterministic hash of file paths + per-file fingerprints
so the cache key flips when the underlying diff changes.
- ErrMissingAPIKey is a sentinel; the HTTP layer maps it to 412 so the
UI knows to surface settings.
- Anthropic API key sourcing in walkthroughCredentials, in order:
ANTHROPIC_API_KEY env, then the user's UI preference
llm.anthropicApiKey, then nothing. Same precedence for the optional
llm.anthropicModel override; default model is claude-sonnet-4-5.
- POST /v1/walkthrough decodes {path, kind, sha, refresh}, loads the
state via ReadRepositoryState or ReadCommitState, checks the cache,
generates if needed, and persists the result via UpsertWalkthrough.
Cache write failures don't block the response — the caller still
gets fresh data.
- New walkthroughs table (repo_root, context_kind, context_sha,
fingerprint, model_id, order_json, notes_json, summary, generated_at)
with composite PK lets one repo cache one walkthrough per context.
CREATE TABLE IF NOT EXISTS plays nicely with existing user DBs.
- store.go GetWalkthrough/UpsertWalkthrough wrap the JSON columns.
Two new pref keys (llm.anthropicApiKey, llm.anthropicModel).
- Four walkthrough_test.go cases cover fingerprint stability +
sensitivity, model-output hallucination filtering, ErrMissingAPIKey,
and prompt assembly.
Bridge / IPC / UI:
- WalkthroughRecord + RepositoryMode tunnel through bridge types,
Electron preload, and the renderer DiffAppApi.
- App.vue gains walkthrough/walkthroughLoading/walkthroughError refs +
a generateWalkthrough(refresh) helper. The CLI -w flag now triggers
a real generation instead of the M2 placeholder.
- Chrome banner gets a Generate/Refresh button next to Copy review,
plus a collapsible block below showing the ordered file list with
per-file notes when a result is present, and the error inline
when generation fails.
Browser fallback returns a static demo walkthrough so the dev shell
keeps working without an API key.
Stages the packaging migration for milestone 7 without cutting the live build. electron-builder keeps shipping today; Forge sits alongside it so the cutover is a config swap once a signed Forge build has been verified against the user's Apple Developer credentials and the homebrew tap repo exists. Shipped in this commit: - packages/ui/forge.config.cjs covers DMG + ZIP makers for darwin arm64 and a GitHub publisher pointed at oullin/git-diff. Code-signing and notarization are wired through APPLE_SIGNING_IDENTITY and the App Store Connect API key env vars so the workflow stays unset-safe locally. - packages/ui/electron/terminal-helper.ts implements installTerminalHelper(): tries /usr/local/bin → /opt/homebrew/bin → ~/.local/bin, writes a launcher script (chmod 755) that resolves existing path args to absolute and exec's open -a "Git Diff Review". - electron/menu.ts gains the "Install Terminal Helper…" menu item under the macOS app menu, between About and Services. - packages/ui/scripts/install-terminal-helper.sh is the standalone launcher template (mirrors what terminal-helper.ts writes) for direct use from the tap recipe if needed. - packages/ui/scripts/Casks/git-diff.rb is the cask source of truth for the still-to-be-created oullin/homebrew-tap. version + sha256 are placeholders the release pipeline overwrites. - docs/distribution.md documents what landed, what's still manual, and the exact pnpm/electron-builder→Forge cutover steps. Out of scope here (need user action): - Creating the oullin/homebrew-tap repository. - The .github/workflows/release.yml workflow (needs Apple notarization secrets configured before it can land green). - Removing the electron-builder devDep + "build" block from package.json. This means the Terminal Helper menu item already works today against the electron-builder DMG; only the Homebrew-cask path stays blocked on the tap repo existing.
`git-diff pr <number>` from the terminal — or window.diffApp.readPullRequest
from the UI — opens an open PR's diff through the same DiffBody that renders
working-tree changes and historical commits. Comments anchor to the PR head
SHA so they stay valid as the branch moves.
Backend (internal/review/pullrequest.go):
- ListPullRequests(launchPath, limit) calls `gh pr list --state open --json
…` and decodes the rows into PullRequestSummary{number,title,author,state,
baseRef,headRef,url}.
- ReadPullRequestState(launchPath, number) calls `gh pr view <n> --json
baseRefOid,headRefOid,…`, fetches origin pull/<n>/head, runs
`git diff --name-status -z --find-renames <base>..<head>` for the file
list, and reuses the same diff-tree-name-status parser as commit mode.
Per-file patches are `git diff <base>..<head> -- <path>`. The resulting
RepositoryState has Mode="commit" + CommitSHA=<headRefOid> + Branch="PR
#<n> (<headRef>)".
- New ErrGhUnavailable sentinel surfaces a 412 from the HTTP layer when the
user doesn't have the gh CLI installed; the UI can map that to a helpful
prompt.
- Generic ghOutput / hasGh helpers added to repository.go so future PR-side
features (review comments, status checks) can ride the same path.
HTTP / bridge / IPC:
- GET /v1/repository/pull-requests?path=&limit= returns the summary list.
- GET /v1/repository/pull-request?path=&number= returns RepositoryState.
- Both routes map ErrGhUnavailable to 412 and other errors to 400.
- Bridge gains listPullRequests + readPullRequest typed client methods.
- Electron preload exposes window.diffApp.listPullRequests/readPullRequest.
- Browser fallback returns an empty list + falls back to the working-tree
state for readPullRequest so the dev shell stays satisfiable.
CLI:
- launch-intent.ts grammar: `git-diff pr <number>` → kind="pull-request".
Validates that PR id is numeric; help text updated.
- App.vue applyLaunchIntent and the live launch-intent listener both
dispatch to a new openPullRequest(number) action when the intent is a
pull-request.
Tests:
- 2 new vitest cases in tests/launch-intent.test.ts cover the happy path
(pr 42 → {kind:"pull-request", prNumber:42}) and the missing-id error
(returns help with explanation). 15 launch-intent tests total.
The Go side ships without a unit test for ListPullRequests /
ReadPullRequestState because they require a working `gh` binary plus a
remote repo to be meaningful; the integration is exercised via the live
HTTP layer.
Comments can now be added before a review session exists. The composer
no longer gates on activeReview; when there is no active review the
comment lands as a draft scoped to (user, repo_root, context_kind,
context_sha). The next "Start Review" click moves every draft into the
new session in a single transaction and clears the local cache.
Backend:
- New pending_comments table keyed by user_id + repo_root + context
(kind + sha). Index on the same tuple keeps the listing query cheap.
- store.go: CreatePendingComment, GetPendingComment, UpdatePendingComment,
DeletePendingComment, ListPendingComments, and PromotePendingComments.
Promotion runs in a single transaction: reads the review's repo + context
tuple, copies matching pending rows into review_comments (prefixing the
pending id with "comment-" to satisfy the PK), and deletes the pending
source row. A mid-promote crash either fully commits or fully rolls back.
- httpx/pending_comments.go wires five routes:
GET /v1/pending-comments?path=&kind=&sha=
POST /v1/pending-comments
PATCH /v1/pending-comments/{id}
DELETE /v1/pending-comments/{id}
POST /v1/pending-comments/promote {reviewId}
All auth-gated; ids are server-generated 8-byte hex.
Bridge + IPC:
- Bridge: listPendingComments, createPendingComment, updatePendingComment,
deletePendingComment, promotePendingComments — typed and exported.
- Electron preload exposes the same shape via window.diffApp.
- Browser fallback returns synthesized echoes so the dev shell remains
satisfiable without the bridge.
UI:
- saveComment in App.vue picks one of two paths: an existing activeReview
still creates a review comment; otherwise it routes through
createPendingComment and refreshes the local pendingComments ref.
- openRepo and startReview both touch the new loadPendingComments helper
so the renderer always reflects what's persisted server-side.
- After a successful startReview the renderer calls promotePendingComments
before fetching the review detail, so the very first reviewDetail GET
already includes the promoted threads.
The visual marker for "this is a draft" is a follow-up — the data model
is in place, but threading the marker through DiffBody.vue's overlay
needs its own focused change.
Pressing Cmd+F (or /) opens a small overlay at the top-right that
searches across every visible diff line. Matches highlight with the
current accent color, Enter / Shift+Enter cycle through them with a
smooth-scroll to center, and Esc closes.
- src/lib/diffSearch.ts: searchDiff queries every
[data-diff-line-text] element under the given root, returns
{matches[], matchedLines}. applyHighlights wraps each match in a
<span class="gd-search-hit"> while preserving the rest of the line's
syntax-highlighted HTML; clearHighlights inverts the wrap. Case
sensitivity is toggleable.
- DiffBody.vue's five `<code v-html=…>` line bodies (split-context,
split-pair-left, split-pair-right, unified-context, unified-added/
-removed) now carry data-diff-line-text so the search treats each
rendered line as one searchable unit. No render churn.
- SearchBar.vue is the overlay. The Cmd+F binding lives in App.vue's
onKeydown (alongside j/k/v/n/p), and the open prop reactively
re-runs the search when query or caseSensitive changes.
- style.css gets two small rules: .gd-search-hit is a soft accent
highlight, .gd-search-hit-active is a darker variant for the
currently-focused match.
- 7 vitest cases in tests/diffSearch.test.ts cover empty query,
case-insensitive vs case-sensitive search, overlapping matches,
non-tagged elements being ignored, and the apply/clear/setActive
roundtrip.
No backend change required.
Backfills the UI half of milestone 8: a PullRequestPicker.vue popover sits next to the History (commits) picker in the chrome banner. Opening it lazily calls listPullRequests, the list renders title, author, and base ← head, and clicking an entry routes through the existing openPullRequest action. - New components/commits/PullRequestPicker.vue mirrors CommitPicker.vue's shape (popover trigger + scrollable result list + loading state + error banner). Surfaces ErrGhUnavailable from the backend as a red-text "gh CLI is required" inside the popover instead of a generic toast. - App.vue gains pullRequests / pullRequestsLoading / pullRequestsError refs, an activePullRequest ref to keep the metadata around for the chrome label, loadPullRequests(), and a small reset path so opening a commit or returning to working tree clears the PR badge. - The chrome banner now reads "PR #42 · <title>" when a PR is loaded, falling back to the commit label otherwise. No backend change needed; the v1 backend from milestone 8 already returned the metadata this picker consumes.
Replaces the inline red error block inside PullRequestPicker with a
dedicated toast in the chrome. The picker now does one thing — list
pull requests — and connection / gh-CLI failures surface beside other
app-level errors.
- App.vue mounts the existing ToastViewport with a small toasts ref +
showToast() / dismissToast() helpers. Default 6s TTL, dismissible
via the X button on the toast itself.
- loadPullRequests() catches into showToast({ tone: "error", title:
"Could not load pull requests", description: cause.message }).
- PullRequestPicker.vue drops the `error` prop and the red-text block
it backed. The component now only renders loading / list / empty.
- This sets up a single error channel that the next milestones (gh
install prompts, missing API keys, etc.) can reuse without each one
reinventing inline error UI.
- override @ungap/structured-clone to ^1.3.1 (deprecated 1.3.0, CWE-502) - rename stale "open workflow log database" error message to "open review database" across auth.go and preferences.go - validate 'limit' query param in repositoryLog and repositoryPullRequests, returning 400 on parse failure
Phase 2-4: introduce packages/contracts as the single source of truth for shared DTOs (auth, repo, review, branch, pullrequest, commit, preference, system, walkthrough, workflow, settings, op, launch). Bridge and UI both depend on it. Delete duplicated bridge/src/types.ts and ui/src/types/api.ts (20+ types lived in both with drift -- UI had ReviewComment.deletedAt that bridge was missing; contracts adds it). Relocate WorkflowBridgeClient to bridge/src/client-types.ts and DiffAppApi to ui/src/types/diff-app.ts so the residual interfaces have proper homes. Drop the @api path alias. Phase 5: split internal/review/repository.go (837 LOC, mixing types, runner, status parsing, patch generation, file IO, branch ops, commit diff) into eight cohesive files in the same package: types, runner, patch, state_reader, commit_reader, commit_log, files, branch. No call-site changes; all Go tests green.
Storage: store.go 1504 -> 246 LOC. Per-domain files now isolate concerns:
users, sessions, preferences, reviews, comments, pending_comments,
repositories, walkthrough (and the existing branches.go gains the Branch
type that used to live in store.go).
httpx handlers: split the two largest god handlers.
- reviews.go (574 LOC) -> repository_state, pull_requests, git_branches,
review_sessions, review_comments (largest 161 LOC).
- auth.go (453 LOC) -> auth_state (AuthState type + constants),
auth_handlers (DTOs + 6 endpoints), auth_middleware (requireAuth +
isPublicAuthPath).
Electron IPC: 593-line flat registry of ~90 ipcMain.handle calls split
into electron/ipc/{repository, repositories, branches, pull-requests,
walkthrough, pending-comments, reviews, workflows, settings, auth, system,
op}.ipc.ts. Each registrar exports register(deps?) and ipc.ts becomes a
28-line orchestrator. Session-token helpers moved to ipc/session-token.ts;
op error envelope inlined to ipc/op.ipc.ts. Replaced the "Gus' MacBook
Setup" user-facing strings with neutral "git-diff" copy. mac* channel
names left in place with a phase-21 rename TODO.
All Go tests pass, vue-tsc and tsc -p tsconfig.electron.json clean.
App.vue carried three side-effect blocks that belong in their own units:
toast queue plumbing, three CSS-applying watchers, and the keyboard
shortcut listener with its selectAdjacent/jumpToHunk helpers.
- useToasts: id-keyed queue with auto-dismiss
- useStyleWatchers: accent / theme / diff-style watchers
- useDiffNavigation: selectAdjacent + jumpToHunk plus useKeyboardShortcuts
that binds j/k/v/n/p/Cmd+Enter/Cmd-F and returns its own unsubscribe
App.vue 1213 -> 1142 LOC; vue-tsc clean. Larger Pinia-backed App.vue
decomposition still to come.
packages/bridge/src/client.ts grew to 487 lines because one HttpWorkflowBridgeClient owned every endpoint body. Move the per-endpoint HTTP wiring out into 12 domain modules under src/clients/ (auth, repository, repositories, branches, pull-requests, reviews, pending-comments, walkthrough, workflows, settings, system, op). Each exports plain functions that take socketPath and request, returning the typed Promise. client.ts now keeps only the public surface: createWorkflowBridgeClient, unixTarget, waitForReady, and a thin HttpWorkflowBridgeClient that delegates every WorkflowBridgeClient method to the matching module. Public API and IPC channel names are unchanged. Bridge tsc clean; vue-tsc + electron tsc + vitest unchanged (still only the pre-existing App.test.ts snapshot failure).
server.go shrank from 215 to 30 LOC: it now holds only the Server struct,
StoreFactory typedef, ServeConfig, and healthz handler. Everything else
moved to its proper home:
- router.go: BuildMux() with routes grouped by resource.
- bootstrap.go: Serve() now delegates to parseServeFlags / openStore /
closeStore / openSocket / resolveOSUsername, each a one-purpose
helper that takes io.Writer for diagnostics and returns the exit
code on failure. The orchestrating Serve body is now 60 lines and
reads top-to-bottom.
Public CLI surface unchanged; all Go tests pass.
Background: packages/storage was a workspace directory used only to host
the Turbo and Vite caches, which made it look like an active package and
left stale macbook-named logs under storage/.cache. Move all cache
locations under .turbo/ (already gitignored) and drop the empty package.
- turbo.json cacheDir: storage/.cache/turbo -> .turbo/cache
- vite.config.ts: ../../storage/.cache/vite/ui -> ../../.turbo/vite/ui
- vite.electron.config.ts: same pattern for ui-electron
- packages/storage: removed entirely
- .gitignore: drop the now-meaningless packages/macbook entries; keep
the top-level storage/.cache + storage/.pnpm-store ignored so an
existing checkout's caches don't dirty the working tree
User-facing surfaces updated: setup.sh now invokes ./packages/api/cmd,
and README.md describes the current packages (api, contracts, bridge,
ui, tools) instead of the old packages/macbook entry.
Linter (oxfmt) re-collapsed several bridge client imports onto single
lines during the format pass; no semantic change.
First slice of the services pattern: a new internal/service package owns
the cross-cutting auth concerns (bcrypt cost, session TTL, minimum
password length) so handlers can stop reaching for golang.org/x/crypto.
AuthService.{State,Setup,Login,Resume,Logout,Wipe} return domain
sentinel errors (ErrPasswordTooShort, ErrPasswordAlreadySet,
ErrPasswordNotSet, ErrInvalidPassword, ErrCannotWipeOtherUser); the
HTTP layer maps each to a status code via a single switch.
Handlers now decode the request DTO, dispatch through AuthService, then
mutate the in-memory AuthState. The fragmented "open store, lookup user,
bcrypt, mutate, write JSON" pattern that repeated five times collapses
to a small adapter per endpoint (auth_handlers.go 366 -> 236 LOC).
Bootstrap.Serve constructs the service from the captured bcryptCost /
sessionTTL / minPasswordLength constants. All Go tests pass.
With the bridge clients gone, the Workflow / RunSummary / RunLog / RunWorkflowRequest / WorkflowEvent / TemplateFile* types under contracts/workflow/ and the OpVault / OpItem / OpUnavailableError types under contracts/op/ no longer have any consumers anywhere in the workspace. Drop both folders and their package.json export entries; the root index.ts loses two re-export lines. contracts/bridge/ui all build clean.
Install pinia, wire the root createPinia plugin in main.ts, and lift the auth session out of App.vue into the first Pinia store. useAuthStore exposes mode, osUsername, currentUser, plus the bootstrap, complete, markWiped, and logout actions; App.vue keeps the cross-domain orchestration (enterApp -> loadPreferences -> refreshRepositoryList -> applyLaunchIntent and the post-logout state reset) because those steps span repo, preferences, and reviews state that still live in App.vue refs. App.vue 1029 -> 1021 LOC; pinia v3 (Vue 3 compatible). vue-tsc + electron tsc clean.
useRepoStore owns the active RepositoryState, activeRepoPath, the loading flag, and the error string -- the four refs that were threaded through every async repository action in App.vue. The store exposes a withBusy() helper so the open/refresh/openCommit/openPR paths can drop their identical try/finally scaffolding in a follow-up. useReviewsStore owns the review list, the currently displayed review detail, and the summary draft the ReviewPanel composes; upsertActive() captures the create-or-update flow used by startReview. App.vue 1021 -> 1023 LOC (composable + store imports replaced eight local refs).
AuthGate wraps the loading / setup / login state-machine that App.vue was rendering inline. The component pulls mode + osUsername off the auth store (via storeToRefs) and emits 'entered' / 'wiped' events so the host can run its cross-domain enterApp orchestration. The default slot only renders once mode === 'ready'. WalkthroughPanel takes (record, error) props and renders the AI walkthrough header / summary / ordered file list. The error case is mutually exclusive with the success case and shares the same chrome. App.vue 1031 -> 993 LOC; vue-tsc clean.
The commit-picker / PR-picker / walkthrough-button / copy-review-button strip that sat between TopBar and the main column was 60 lines of template logic in App.vue. Lift it into RepoToolbar.vue with the lists, loaders, and active pointers as props and seven emit events the host wires to its existing actions (loadCommits, openCommit, returnToWorkingTree, loadPullRequests, openPullRequest, generateWalkthrough, copyReviewAsMarkdown). App.vue 993 -> 953 LOC; vue-tsc clean.
The hero / loading / error trio that the diff column renders before a
RepositoryState is available was 55 lines of template (with three icons
and two CTA buttons) inline in App.vue. Lift it into RepoEmptyStates
parameterised by 'kind' so the host picks which of the three to show
('hero' / 'loading' / 'error'); the component emits add-repository and
open-last-repo events the host wires to its addRepository / openRepo
actions.
The lucide-vue-next icons (GitPullRequest, FolderOpen, Plus) moved with
the component, so App.vue can drop that import entirely.
App.vue 953 -> 905 LOC; vue-tsc clean.
The diff column was rendering three exclusive branches inline: FileContentViewer fallback for non-changed paths, a "no changes" placeholder, and a v-for over files with FileHeader + DiffBody cards. That whole subtree moves into DiffList.vue which takes the file array, the selection refs, the layout maps, the tweaks bundle, the review comments, and the predicate functions as props -- it emits per-file events (toggle-collapsed, toggle-viewed, copy-path, open-comment-for- line, delete-comment, reply-comment, update:split-ratio) that App.vue forwards to its existing actions. App.vue 905 -> 874 LOC. Dropped FileContentViewer / FileHeader / DiffBody from App.vue's import block since the subtree owns them now. vue-tsc clean.
The "Copy review as Markdown" toolbar button maintained its tri-state status (idle / copied / error) and the 1.5s auto-reset timer inline in App.vue. Move the lot into useReviewMarkdownCopy: the composable owns the state ref and the copy(detail) action; the onError callback writes the cause back into App.vue's error ref so the toast surface stays consistent. formatReviewAsMarkdown is now only imported inside the composable. App.vue 874 -> 867 LOC; vue-tsc clean.
The openRepo / refresh / openCommit / openPullRequest / switchBranch functions all carried the same try / finally scaffolding for the loading + error refs. Drop the bookkeeping by routing each through repoStore.withBusy: the store sets loading on entry, clears it on exit, and resets error.value at the start of the action. The catch arms keep their domain-specific error mapping (parseBridgeError dirty-tree detection in switchBranch, optimistic state.value = null on openRepo). vue-tsc clean.
ReviewService picks up the second slice of the services pattern: it owns review-session, review-event, and review-comment use cases. Construction takes a *storage.Store directly; the service hides randomID generation (so callers can pass an empty input.ID) and lets the storage layer keep emitting the comment_added / comment_edited / comment_deleted side events. Handlers in review_sessions.go / review_comments.go thin out considerably: each is now decode -> dispatch -> writeJSON with a single switch on service.ErrAuthenticationRequired for the 401 path. Bootstrap wires reviewSvc into Server.ReviewService alongside AuthService. All Go tests pass.
PendingCommentService takes the third slice of the services pattern. List / Create / Update / Delete / Promote use cases all live in the service; handlers stop reaching for the store handle and hand-rolling randID + repoRoot defaults + filePath/diffSection validation. New domain errors -- ErrInvalidCommentInput and ErrReviewIDRequired -- ride alongside ErrAuthenticationRequired from review.go so the http handler can switch once and translate to 400 / 401 / 500. pending_comments.go 227 -> 145 LOC. randID moved into the service. go test ./... clean.
RepositoryService consolidates two adjacent surfaces that share the owner-check contract: the repository registry (list / upsert / remove) and collaborator management (list / grant / revoke). All five use cases live on the service; the new ErrRepositoryPathRequired sentinel lets handlers translate the missing-query-param case to 400 without hand-rolling the check. repositories.go 122 -> 85 LOC, collaborators.go 146 -> 112 LOC. The storage.ErrRepositoryNotOwned / ErrRepositoryNotFound errors flow through unchanged for the 403 / 404 translations. go test ./... clean.
PreferenceService captures the per-user UI preferences use cases. The storage layer already implements the optimistic merge + delete-on- empty semantics; the service is a thin owner that lets the get / save preferences handlers drop their identical store-handle blocks. preferences.go shrinks; vue-tsc and go test ./... clean.
BranchService owns the lock / unlock / delete use cases that combine git state (resolve repo root via review.ResolveRoot, run git delete) with the DB-backed lock table. The service hides the resolve-then-act two-step that every handler was duplicating. New domain errors: ErrBranchNameRequired (400) and ErrBranchAlreadyLocked (409). branches.go 192 -> 99 LOC. go test ./... clean.
A previous commit kept the service package imported in server_test.go with a "var _ = service.NewAuthService" suppression hack as a hook for future tests. Remove it -- the file is happier without dead imports and any future test that needs a service-wired server can add the import then.
The previous "introduce ReviewService" commit advertised this change but the Write that rewrote review_sessions.go didn't take (file-state staleness, not caught at the time). Re-apply the rewrite: createReview, listReviews, reviewDetail, and addReviewEvent now decode -> dispatch -> writeJSON with a single switch on service.ErrAuthenticationRequired. The local randomID helper moves into service/review.go where it belongs. review_sessions.go 161 -> 100 LOC; go test ./... clean.
repositoryOpen kept the last hand-rolled s.Store(...) call in the handler layer: after reading the working-tree state it would save the last-opened repo into ui_preferences and upsert the repository row. Replace both calls with PreferenceService.Save and RepositoryService.Upsert so the handler stops reaching for the store factory directly. Build + tests clean.
BranchService gains a List(path) method that handles the read-git + sync-db-state + read-db-records dance the handler was open-coding. The new ListResult type bundles the git names with the optional DB records so the JSON payload structure stays identical. When the store-side path fails the result still contains the git names so the UI degrades gracefully (matching the original behaviour). git_branches.go drops the s.Store(...) call and the closeStore / SyncBranches / ListBranches three-step. Build + tests clean.
The file-search handler enumerated the user's known repositories with a direct s.Store(...) call. Replace it with RepositoryService.List so the handler stops reaching for the store factory and the auth check flows through the service's ErrAuthenticationRequired sentinel. Build + tests clean.
WalkthroughService captures the cache-lookup + credential-resolution + LLM-call orchestration the handler was inlining. The new GenerateRequest carries the resolved RepositoryState plus the request parameters; the result bundles the persisted record with a Cached flag so the caller can record whether it hit cache. The credential helper (env-vars-win-over-preferences) moves with it. New domain error: ErrAnthropicNotConfigured (the 412 case). walkthrough.go 163 -> 98 LOC; build + tests clean.
With every handler now routed through a service, nothing reads from the StoreFactory anymore. Remove the field, the StoreFactory typedef, and the bootstrap closure that captured the singleton store. The context import is no longer needed in server.go either; bootstrap.go still uses it for the parseServeFlags call. server_test.go drops the inline storage open / close closure; the helper now constructs a Server with only the fields the test needs (Home / Repo / Settings / Auth). Healthz test still passes. Build + tests clean.
Tokenization for diff lines now runs in a pool of Web Workers (min(hardwareConcurrency - 1, 4)) instead of blocking the renderer. highlightLine keeps its synchronous signature: cache hit returns the tokenized HTML, cache miss returns escaped plaintext and dispatches an async request that bumps highlighterRev when it lands so Vue re-renders into the cached tokens. Adds an LRU cache (5000 entries, keyed by theme:lang:text) to skip repeated tokenization on scroll-back and view toggles. Theme switches flush the cache. Falls back to a main-thread highlighter when Worker is unavailable (vitest happy-dom). Long-line guardrails skip wordHi on lines over 2000 chars and skip shiki tokenization above 20000 chars, returning plaintext to avoid stalling on minified or generated content.
Each hunk header now exposes expand-up / expand-down buttons that fetch the lines suppressed by the unified diff format. Lines stream back from a new GET /v1/repository/file-range endpoint that reads either the working tree (default) or `git show <ref>:<path>` when a commit ref is supplied. Responses are capped at 500 lines per call and surface an eof flag so the last hunk's expand-down stops at the end of the file. The UI side: patch.ts gains getHunkInfos, parseHunkHeader, and applyExpansions for splicing fetched context into the parsed line list. splitPatchLines lets DiffBody build split rows from the merged unified list without re-parsing. A useContextExpansion composable holds per-section state (expansions, inflight flags, downward EOF) and drives the buttons; spinners and disabled states reflect that state. RepositoryFileRange flows through contracts, bridge, IPC, preload, and the DiffApp type so the renderer can call it the same way as every other bridge method.
A new Eye toggle on the file header swaps the diff view for a rendered markdown preview when the file matches *.md or *.markdown and the file isn't deleted. Lines that the diff adds in new-file space are tinted green in the preview so reviewers can see which prose changed, even though they're reading the rendered output. MarkdownPreview.vue pulls the file via readRepositoryFile in working mode or readRepositoryFileRange when a commit ref is supplied. The renderer applies marked.parseInline line-by-line through renderMarkdownWithLineAnchors so each output block carries a data-line attribute; getAddedLineNumbers walks the patch to drive the highlighting. DOMPurify sanitises every rendered chunk before v-html mounts it. Preview state lives in useDiffLayout alongside collapse/split, so toggling persists across rerenders within a session.
Reviewers can now anchor a comment to a range of lines instead of a single line, including ranges that cross the deletion / addition boundary in split view. Click the "+" gutter button to set the anchor, then shift-click another line's "+" to commit the range and open the composer. Comments render their range as "Old line 12 → New line 18" or "New lines 12–18" depending on shape; single-line comments keep their existing label. Storage gains nullable start_line_number / start_side columns on review_comments and pending_comments. addCommentRangeColumns walks each table on Init and adds them via ALTER if missing, so existing databases pick the new fields up automatically. The promote tx copies them when a draft becomes a finalised review comment. The fields propagate through contracts, the bridge client, the IPC preload layer, and the diff-app type, so the renderer can pass them on every createReviewComment / createPendingComment call without type gymnastics. useLineSelection holds the anchor + range arithmetic (normalising direction so the label always reads top-to-bottom and left-then-right).
No description provided.