Skip to content

chore: better ui/ux#1

Merged
gocanto merged 71 commits into
mainfrom
feat/retire-macbook-extract-api
May 21, 2026
Merged

chore: better ui/ux#1
gocanto merged 71 commits into
mainfrom
feat/retire-macbook-extract-api

Conversation

@gocanto

@gocanto gocanto commented May 20, 2026

Copy link
Copy Markdown
Contributor

No description provided.

gocanto added 4 commits May 20, 2026 11:52
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".

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pnpm-lock.yaml Outdated
Comment thread packages/api/internal/app/httpx/auth.go Outdated
Comment thread packages/api/internal/app/httpx/reviews.go Outdated
gocanto added 16 commits May 20, 2026 13:34
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
@gocanto gocanto changed the title Feat/retire macbook extract api chore: better ui/ux May 20, 2026
gocanto added 8 commits May 20, 2026 15:17
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.
gocanto added 27 commits May 20, 2026 16:18
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).
@gocanto gocanto marked this pull request as ready for review May 21, 2026 08:40
@gocanto gocanto merged commit 118e09c into main May 21, 2026
@gocanto gocanto deleted the feat/retire-macbook-extract-api branch May 21, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant