Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
180 commits
Select commit Hold shift + click to select a range
37e62b5
feat(desktop): visibility, flexibility and reach across every GitHub …
antonarnaudov Aug 26, 2026
9943ce1
fix(desktop): overlay lifecycle and one vocabulary for the four lists
antonarnaudov Aug 27, 2026
c001ba5
feat(desktop): PR state control, and an Actions run page that shows t…
antonarnaudov Aug 27, 2026
b348181
fix(desktop): log viewer, Organizations, and the safeguards that weren't
antonarnaudov Aug 27, 2026
c5e41ba
fix(desktop): per-surface layout — Changes, Commits, Branches, Compar…
antonarnaudov Aug 27, 2026
fc728c6
fix(desktop): draw the settings checkbox from the app's own tokens
antonarnaudov Aug 27, 2026
3b37d73
fix(desktop): the last two broken items — tag peek dead end, log expand
antonarnaudov Aug 27, 2026
dfac9b5
fix(desktop): the confusing tier — things that worked but misled
antonarnaudov Aug 27, 2026
0869813
fix(desktop): close the half-fixes the verification pass caught
antonarnaudov Aug 27, 2026
125355e
test(desktop): a functional harness, and fixtures that can answer mor…
antonarnaudov Aug 27, 2026
568aba3
fix(desktop): row meta as columns, and states you can actually see
antonarnaudov Aug 27, 2026
5c8edb6
fix(desktop): Workflows rows that say something, and a Settings rhythm
antonarnaudov Aug 27, 2026
9e193e0
fix(desktop): Explore people rows, Projects and Gists headers
antonarnaudov Aug 27, 2026
cfbfb55
fix(desktop): Changes toolbar and rows, run-row columns — and guards …
antonarnaudov Aug 27, 2026
ef21ff8
fix(desktop): one vocabulary for divergence, people, dock chrome, row…
antonarnaudov Aug 27, 2026
21e8f24
fix(desktop): meta columns across every list, empty states that answe…
antonarnaudov Aug 27, 2026
e6cf782
fix(graph): make the CHANGES column comparable, and give the harness …
antonarnaudov Aug 27, 2026
f0b1912
fix(desktop): Settings is a form, so give it a form's width and a rea…
antonarnaudov Aug 27, 2026
5f09b0c
fix(desktop): the last of the sweep — collapsed rail groups, a Status…
antonarnaudov Aug 27, 2026
a3e5390
harness: a probe mode, so a surface can be measured without editing t…
antonarnaudov Aug 27, 2026
2e64ea3
fix(desktop): stop an unrelated cache bust from pinning a list forever
antonarnaudov Aug 27, 2026
c2ebb50
fix(desktop): one rule for "is this the same repo", and report the re…
antonarnaudov Aug 27, 2026
3e17dd8
fix(desktop): make the markdown sanitizer fail closed — it was passin…
antonarnaudov Aug 27, 2026
7a109e7
fix(host-bridge): close three leaks in the crash-report scrubber
antonarnaudov Aug 27, 2026
feb3c28
harden(desktop): build the graph tile’s action button from nodes, not…
antonarnaudov Aug 27, 2026
b426034
test(desktop): make "guard renderer input before it reaches git" a pr…
antonarnaudov Aug 27, 2026
8f01f25
fix(desktop): keep credentials out of the Output tab's git log
antonarnaudov Aug 27, 2026
73a9843
fix(desktop): a CI progress bar showed every state at once in the log…
antonarnaudov Aug 27, 2026
6ef19f2
fix(ext): the AI panel's esc() left the quote alone, so a link URL es…
antonarnaudov Aug 27, 2026
878fbb8
fix(desktop): Amend promised a new commit and rewrote the last one
antonarnaudov Aug 27, 2026
866e479
fix(desktop): a narrow window should cost room, not controls
antonarnaudov Aug 27, 2026
71d9e77
fix(desktop): the keyboard follows you, the PR Files tab is reachable…
antonarnaudov Aug 28, 2026
9e2cc3f
fix(desktop): the identity chips in Explore rails were dead buttons
antonarnaudov Aug 28, 2026
3a966d8
fix(desktop): the branch switcher switches branches, staging keeps yo…
antonarnaudov Aug 28, 2026
f8cf933
refactor(desktop): the app stops opening on a file tree, and reposito…
antonarnaudov Aug 28, 2026
e2eb9fe
fix(desktop): the controls stop lying, and the layout stops moving un…
antonarnaudovpro Aug 28, 2026
609d16d
test(harness): probe.mjs kills a hung page at 90s, like check.mjs
antonarnaudovpro Aug 28, 2026
b9818b8
fix(desktop): the Projects drawer is a real modal, and a dismissible one
antonarnaudovpro Aug 28, 2026
9ad654b
fix(desktop): what a second sweep found — including four things the f…
antonarnaudovpro Aug 28, 2026
f12c7a9
fix(desktop): two clicks could commit conflict markers, and a broken …
antonarnaudovpro Aug 29, 2026
ffe8a38
fix(desktop): line staging edited the wrong lines, and shipped script…
antonarnaudovpro Aug 29, 2026
be00e3a
fix(desktop): a modify/delete conflict had no resolution the app coul…
antonarnaudovpro Aug 29, 2026
b776b64
fix(desktop): Compare answered for refs it had not compared, and coun…
antonarnaudovpro Aug 29, 2026
753170d
fix(desktop): the app answering for work it had not done — and losing…
antonarnaudovpro Aug 29, 2026
634c23b
fix(desktop): the last of the sweep — a file listed twice, and three …
antonarnaudovpro Aug 29, 2026
18ea976
fix(desktop): the re-sweep — including six regressions from the last …
antonarnaudovpro Aug 29, 2026
07df2fb
fix(desktop): a dialog that only CLAIMED to hold the page, and a Back…
antonarnaudovpro Aug 29, 2026
e99a9f6
fix(git): three ways a rebase or a line-stage did the wrong thing and…
antonarnaudovpro Aug 29, 2026
f3b49e1
fix(git): the reword queue I added, and the four ways it was wrong
antonarnaudovpro Aug 29, 2026
bada33a
fix(git): stop describing the queue's lifetime and let git own it
antonarnaudovpro Aug 29, 2026
60119c5
fix(git): a patch already upstream wedged the rebase, and the suite w…
antonarnaudovpro Aug 29, 2026
5082a81
polish(desktop): what a screen reader hears, and three things measure…
antonarnaudovpro Aug 29, 2026
62af713
fix(desktop): Compare called a file it could not read "identical"
antonarnaudovpro Aug 29, 2026
99c7b68
fix(desktop): the theme fix threw away the form beside it, and Settin…
antonarnaudovpro Aug 29, 2026
50fdfdb
fix(desktop): three ways line staging destroyed a file, and four thin…
antonarnaudovpro Aug 30, 2026
7b72e0a
fix: the last fourteen findings, and the fixtures that hid four of them
antonarnaudov Aug 30, 2026
a4a7f5c
fix: what the re-sweep found, including the regression the last commi…
antonarnaudov Aug 30, 2026
e04e1ce
fix: two ways the last commit destroyed data, and the layer question …
antonarnaudov Aug 30, 2026
bb6e92b
fix: the mid-operation state table, and the five ways I was deciding …
antonarnaudov Aug 30, 2026
dfaf67e
test: a census of every destructive control's double-click guard
antonarnaudov Aug 30, 2026
db695f1
fix(harness): teach the opState fixture the shape the host now returns
antonarnaudov Aug 30, 2026
00e64b5
harness: make the bugs Anton found expressible, and write them down a…
antonarnaudov Aug 31, 2026
e1516ff
fix(nav): the back button pops the history instead of pushing onto it
antonarnaudov Aug 31, 2026
81a9f32
feat(commit): a commit is a page, not a row to reveal in the graph
antonarnaudov Aug 31, 2026
d0f4837
fix: three defects that lied to you, and two guards so they cannot recur
antonarnaudov Aug 31, 2026
961d722
fix(log): a document you can read, not a slit you scroll past
antonarnaudov Aug 31, 2026
f503a8c
fix(orgs): pointing at a repo no longer hides what the repo says
antonarnaudov Aug 31, 2026
84d1280
feat(compose): one markdown editor, and drafts that survive Escape
antonarnaudov Aug 31, 2026
42da5a9
fix(releases): publishing is a decision, not a checkbox
antonarnaudov Aug 31, 2026
45109d6
feat(commit): a filter, because at 420 files scrolling is not finding
antonarnaudov Aug 31, 2026
7cf1da1
fix(prs): a renamed file says what it was renamed from
antonarnaudov Aug 31, 2026
68d506e
test: measure three defect classes on every view, not one screenshot …
antonarnaudov Aug 31, 2026
086ac3a
feat(desktop): the log, the release and the issue each get a page
antonarnaudov Sep 1, 2026
f8144c6
refactor(prs): editing a pull request gets the composer page too
antonarnaudov Sep 1, 2026
5a4f45e
fix(compose): a restored draft says so, and a new release lands on it…
antonarnaudov Sep 1, 2026
2ead206
fix(releases): a draft leads with publishing it
antonarnaudov Sep 1, 2026
09df83a
chore: untrack the stray "~" directory
antonarnaudov Sep 1, 2026
fd91c10
docs: say which unit the commit dates are in, where they are declared
antonarnaudov Sep 1, 2026
0bd3b25
docs(shortcuts): the log page's keyboard is in the cheat sheet
antonarnaudov Sep 1, 2026
36353d3
fix(desktop): the log stops scrolling for you, the diff shows or says…
antonarnaudov Sep 1, 2026
ab95f34
fix(changes): the working tree's diff was never once exercised
antonarnaudov Sep 1, 2026
9cd8d8e
fix(logs): End goes to the last line without claiming to follow
antonarnaudov Sep 1, 2026
ae6ca0f
fix: Compare's commit list can be scrolled, and a signed-in user is n…
antonarnaudov Sep 2, 2026
01d8843
fix(commits): the list keeps its height so the scroller has something…
antonarnaudov Sep 2, 2026
d59a255
feat(refs): a tag can be deleted and pushed, and a dead upstream says so
antonarnaudov Sep 2, 2026
cc1c8c8
feat(refs): the ref read carries what it already knew
antonarnaudov Sep 2, 2026
e45afb3
feat(branches): the ref manager, one kind at a time
antonarnaudov Sep 2, 2026
db6ea7f
fix(keys): one Escape dismisses one layer, over a page as well as a peek
antonarnaudov Sep 2, 2026
bd92ccd
feat(branches): the rest of it — facets, the age cut, sort, worktrees…
antonarnaudov Sep 2, 2026
ae817f8
fix: a branch checkout must attach, and a commit's verbs must run
antonarnaudov Sep 2, 2026
b914bb5
fix: the branch sweep offered to delete the default branch
antonarnaudov Sep 2, 2026
4ed6413
fix: the current branch appeared under Stale, and the fixture had not…
antonarnaudov Sep 2, 2026
fbab14f
polish: Compare's commit list gets a measure
antonarnaudov Sep 2, 2026
f34940c
fix: 22 defects from the adversarial sweep of tonight's work
antonarnaudov Sep 2, 2026
dc900b0
test: pin the diff path's two fighting properties
antonarnaudov Sep 2, 2026
888d36c
fix: the error map drifted out of line with the log
antonarnaudov Sep 2, 2026
ef28107
fix: a second adversarial sweep — the lists, staging, releases
antonarnaudov Sep 2, 2026
0b26ab0
fix: Commit over a clean tree, and one order for every harvested facet
antonarnaudov Sep 2, 2026
77c4d82
fix: a cancelled run is not a failure, and a live run says how long
antonarnaudov Sep 2, 2026
0b26b39
fix: an emptied branch list, a bar that walked off the page, and ANSI…
antonarnaudov Sep 2, 2026
8087982
fix: Escape discards a label picker, and the PR one batches like the …
antonarnaudov Sep 2, 2026
04624ef
fix: one key press closes one layer, and never drops the keyboard
antonarnaudov Sep 2, 2026
5626d39
polish: the light accent ink, an error tick inside its map, the ? sheet
antonarnaudov Sep 2, 2026
3e8f917
fix: the palette no longer discards your arrow keys when results land
antonarnaudov Sep 2, 2026
c3a3a3e
fix: a read notification's text was faded twice, to 3.04:1 in light
antonarnaudov Sep 2, 2026
9b64141
fix: the palette's selection has two opposite right answers, not one
antonarnaudov Sep 2, 2026
e25a367
fix: the conflict-marker guard only applies to unmerged paths
antonarnaudov Sep 2, 2026
a68a2f7
test: the ref:log fixture honours maxCount, so the stash page is chec…
antonarnaudov Sep 2, 2026
992f3c7
chore: untrack a build artifact that `git add -A` swept in
antonarnaudov Sep 2, 2026
d4af7c5
docs: the pr:labels undefined was the harness, not the app
antonarnaudov Sep 2, 2026
15f491e
fix: the log's five states, found by giving it a state table
antonarnaudov Sep 2, 2026
8d7de4e
fix: six Branches cells the state table found
antonarnaudov Sep 2, 2026
92eb9bf
fix: five ways the app trapped you or threw your work away
antonarnaudov Sep 2, 2026
8651967
fix: four ways the Code and Explore views mislaid what you were reading
antonarnaudov Sep 2, 2026
9014e53
fix: the Commits graph — typing paints, Enter travels, j/k move
antonarnaudov Sep 2, 2026
2d15d9f
fix: Switch account switches, and a dead recent can be forgotten
antonarnaudov Sep 2, 2026
6a408ff
fix: a dead peek action, and tab strips that walked off the page
antonarnaudov Sep 2, 2026
6713b6b
fix: the Code view's Refresh, and an empty subfolder that said nothing
antonarnaudov Sep 2, 2026
a8653dc
fix: prune on fetch, and a graph selection that outlived its rows
antonarnaudov Sep 2, 2026
af242de
fix: a contradicted count, a blank pane, and an unstyled row
antonarnaudov Sep 2, 2026
c776238
fix: the Projects board's stale repaint and its unlabelled draft menu
antonarnaudov Sep 2, 2026
28d4981
fix: "Show in the graph" says so when it cannot
antonarnaudov Sep 2, 2026
d93e0a2
test: pin the clone trap, driven end to end
antonarnaudov Sep 2, 2026
8ccebe5
fix: in a rebase, "Take ours" was handing you the upstream
antonarnaudov Sep 2, 2026
884d5a0
fix: "Mark resolved" could write the merge base over both sides' work
antonarnaudov Sep 2, 2026
7cd3fd7
fix: one hunk tick could settle a whole conflict
antonarnaudov Sep 2, 2026
cc3613c
fix: the dock covered the last screenful of every detail page
antonarnaudov Sep 2, 2026
58b5db0
fix: five defects introduced by tonight's own fixes
antonarnaudov Sep 2, 2026
041ae25
fix: the Assistant — ten defects behind a gate nothing could open
antonarnaudov Sep 2, 2026
23d1bbc
fix: the diff panel drew six different nothings the same way
antonarnaudov Sep 2, 2026
1d35d28
fix: conflicts that are not content conflicts, and the dock's dead ends
antonarnaudov Sep 2, 2026
2241f6a
fix: a confirm that leaves its own trigger live stacks one dialog per…
antonarnaudov Sep 2, 2026
ec9def8
fix: read conflict stages the way this file says to
antonarnaudov Sep 2, 2026
cd088be
fix: a claim set after an await is not a claim
antonarnaudov Sep 2, 2026
44d8f72
test: pin the queued→running note, and delete the check that could no…
antonarnaudov Sep 2, 2026
182f85a
test: pin the transcript against the agent's own commit
antonarnaudov Sep 2, 2026
ab9c955
test: give the harness a real PTY id, and pin the dead shell
antonarnaudov Sep 2, 2026
10a1774
test: pin the Output filter, now that git:log can be driven
antonarnaudov Sep 2, 2026
2e4e5fe
test: pin the two conflicts that have no text to merge
antonarnaudov Sep 2, 2026
0f16006
test: pin the dock keyboard hand-back and the buried footer
antonarnaudov Sep 2, 2026
9a6e8c1
fix: five neighbour-state failures in the last batch, found by sweep 5
antonarnaudov Sep 2, 2026
ce5af6c
fix: the diff panel's new guards, told what they were guessing at
antonarnaudov Sep 2, 2026
0d1d7c0
fix: eight more from sweep 5 — the Assistant gate, and four leaks of the
antonarnaudov Sep 2, 2026
62566cd
fix: three more from sweep 5 — the binary that would not say what hap…
antonarnaudov Sep 2, 2026
78f5661
fix: two surfaces that moved without being asked
antonarnaudov Sep 2, 2026
493449c
fix: the last three from sweep 5
antonarnaudov Sep 2, 2026
72bab0d
fix: three neighbour-state holes in my own last batch
antonarnaudov Sep 2, 2026
f11a7c1
fix: keep the scroll snapshot out of Monaco
antonarnaudov Sep 2, 2026
62dbb98
perf: measure before allocating in showAt
antonarnaudov Sep 2, 2026
d925013
test: fixture the three channels every check was missing
antonarnaudov Sep 2, 2026
34ddc06
test: pin the sentence a human reads before approving an agent's write
antonarnaudov Sep 2, 2026
5499ee3
fix: `git am` was given the rebase labels, and they say the opposite
antonarnaudov Sep 2, 2026
f261900
test: prove the cherry-pick and revert sides against real git too
antonarnaudov Sep 2, 2026
4c53c35
test: emit the event shapes the main process actually sends
antonarnaudov Sep 2, 2026
b642634
test: pin that a declined action does not read as a failure
antonarnaudov Sep 2, 2026
722e49e
fix: the button that deletes a file looked exactly like the one that …
antonarnaudov Sep 2, 2026
be5f1b4
fix: the commit page leaked a Monaco diff editor per visit
antonarnaudov Sep 2, 2026
b66b0e6
fix: three Assistant controls that looked live and answered with silence
antonarnaudov Sep 2, 2026
427fb8f
fix: a failed turn kept its reply looking like it was still being typed
antonarnaudov Sep 2, 2026
a758e91
fix: five more neighbours, four of them in this session's own fixes
antonarnaudov Sep 2, 2026
866e2f8
fix: Forward died on every file save, and two keyboard dead ends
antonarnaudov Sep 2, 2026
4bc6f48
fix: the Rebase view landed commits one row off, and would run a plan
antonarnaudov Sep 2, 2026
5cbe6f0
fix: four in Compare, including a control that never came back
antonarnaudov Sep 2, 2026
c4c5fd1
fix: three from sweep 6's keyboard and state-table lenses
antonarnaudov Sep 2, 2026
d910953
fix: three light-theme failures, two of them mine from this session
antonarnaudov Sep 2, 2026
44dbea0
fix: the graph resizer's floor was a stale copy of the graph's own br…
antonarnaudov Sep 2, 2026
a4e9d96
fix: the guard I added an hour ago was dead code, and the app's minimum
antonarnaudov Sep 2, 2026
f1fe037
fix: branch rows rendered their actions outside the window
antonarnaudov Sep 2, 2026
74e01e7
fix: three that outlived what they belonged to
antonarnaudov Sep 2, 2026
26fb6c2
fix: two surfaces that described a state they were not in
antonarnaudov Sep 2, 2026
e4c36d5
fix: the last four from sweep 6
antonarnaudov Sep 2, 2026
95549d6
fix: five more of my own, four of them from the last two hours
antonarnaudov Sep 2, 2026
dcd79f5
fix: two of the owner's own complaints, still live through other doors
antonarnaudov Sep 2, 2026
00249b8
fix: four in conflict resolution, one of them still able to lose work
antonarnaudov Sep 2, 2026
23c4a5a
fix: three that only ever moved in one direction
antonarnaudov Sep 2, 2026
02f331f
fix: three keyboard and dialog dead ends
antonarnaudov Sep 2, 2026
590a0f6
fix: "Switch account" was still a quieter Sign out
antonarnaudov Sep 2, 2026
fe247de
fix: the last four from sweep 7
antonarnaudov Sep 2, 2026
e4900e8
fix: resolving a conflict with no common ancestor added a blank line
antonarnaudov Sep 2, 2026
db1843e
fix: the whitespace toggle meant two different things in the two diff…
antonarnaudov Sep 2, 2026
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,13 @@ release/
# Claude Code's per-repo scratch area. It holds nested git worktrees, so a single
# `git add -A` would otherwise commit entire checkouts into this repo.
.claude/

# A literal "~" directory, created by an unquoted ~ in a shell command and
# then swept in by `git add -A`. It held npm logs, not source.
~/

# A 584KB minified main-process bundle that `npm run package` drops beside
# package.json. The real output is dist/main/main.js (which package.json's
# "main" points at, and which dist/ already ignores); this copy is a build
# side-effect and `git add -A` swept it in.
apps/desktop/main.js
2 changes: 2 additions & 0 deletions apps/desktop/harness/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
page/
out/
37 changes: 37 additions & 0 deletions apps/desktop/harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Headless render harness

Runs the WHOLE desktop renderer in plain headless Chrome against realistic
fixtures — no Electron, no GitHub, no repo. This is how UI work on the desktop
app gets **seen** before it ships: every view, both themes, list and detail
states, driven by scripted scenes.

```sh
cd apps/desktop
node esbuild.js # build the renderer bundles
harness/gen.sh # assemble harness/page from dist + shim.js
harness/shot.sh issues out/issues.png # screenshot a scene
harness/shot.sh 'issues~open31' out/detail.png # …with driver steps
harness/shot.sh 'prs~open106~click:.gh-subtab%5Bdata-sub%3Dfiles%5D' out/files.png
harness/shot.sh issues out/light.png light # light theme
```

## Functional checks

Screenshots prove a surface renders. They do not prove the count badge tracks
the filter, that a disabled button is disabled, that a menu is dismissed on
navigation, or that two columns share an x — those are assertions.

```sh
node harness/check.mjs # every case
node harness/check.mjs count palette # only cases matching these substrings
```

Each case names a scene and an assertion from `checks.js`, which runs INSIDE
the page after the scene driver finishes and returns a list of failures. The
runner exits non-zero if anything fails, so it can gate a commit. Add a case by
writing the assertion in `checks.js` and listing `[id, scene]` in `check.mjs`.

`shim.js` fakes the preload's `window.gitstudio` bridge (see `shared/ipc.ts`)
with fixtures for the GitStudio repo itself. Unstubbed channels log
`[shim missing] <channel>` to the console and resolve safely — add a fixture
when a view needs one. `harness/page/` and `harness/out/` are generated.
548 changes: 548 additions & 0 deletions apps/desktop/harness/check.mjs

Large diffs are not rendered by default.

8,818 changes: 8,818 additions & 0 deletions apps/desktop/harness/checks.js

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions apps/desktop/harness/gen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/bin/sh
# Assemble the headless-render harness page from the built renderer bundles +
# the fixtures shim. Run `npm run build` (or `node esbuild.js`) first.
#
# gen.sh [outDir]
#
# The optional outDir lets a second page exist alongside the default one, so a
# verification run can use a freshly built bundle while something else is still
# reading the old one. shot.sh and probe.mjs both honour $GS_HARNESS_PAGE.
set -e
HARNESS="$(cd "$(dirname "$0")" && pwd)"
# BUILD FIRST. This script only COPIES the bundle, and forgetting the build
# before it is the single most expensive mistake in this harness: a check runs
# against the previous bundle, and reports a fix as not working (or, worse, a
# reverted fix as still working, which is a negative test that lies). Set
# GS_NO_BUILD=1 to skip it when you have just built by hand.
if [ -z "$GS_NO_BUILD" ]; then
(cd "$HARNESS/.." && node esbuild.js >/dev/null)
fi
DIST="$(cd "$HARNESS/../dist/renderer" && pwd)"
PAGE="${1:-$HARNESS/page}"
rm -rf "$PAGE"
mkdir -p "$PAGE"
cp "$DIST/renderer.js" "$DIST/renderer.css" "$DIST/theme-boot.js" "$PAGE/"
cp "$DIST"/brand-*.svg "$DIST"/icon*.png "$PAGE/" 2>/dev/null || true
cp "$HARNESS/shim.js" "$PAGE/shim.js"
cp "$HARNESS/checks.js" "$PAGE/checks.js"
cat > "$PAGE/harness.html" <<'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="./renderer.css" />
<title>GitStudio harness</title>
</head>
<body>
<script src="./theme-boot.js"></script>
<div id="root"><div id="boot">Loading GitStudio…</div></div>
<script src="./checks.js"></script>
<script src="./shim.js"></script>
<script src="./renderer.js"></script>
</body>
</html>
HTML
echo "harness page at $PAGE/harness.html"
139 changes: 139 additions & 0 deletions apps/desktop/harness/probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env node
// Ask a rendered scene a question.
//
// shot.sh shows you what a surface LOOKS like; check.mjs asserts a named
// invariant. Neither lets you simply measure something — "how wide is that
// column", "does that button have an accessible name", "what does the focus
// ring compute to in light theme" — without first editing the shared checks
// file, which is impossible when several people are investigating at once.
//
// node harness/probe.mjs '<scene>' '<js>' [--theme=light] [--width=1150]
// [--extra=staging=checkboxes]
//
// The <js> body runs INSIDE the driven page after its steps have played, with
// `await` available; whatever it returns is JSON-printed. Helpers in scope:
//
// $(sel[, root]) querySelector
// $$(sel[, root]) querySelectorAll as an array
// box(sel|el) {x,y,w,h,right,bottom} rounded, or null
// css(sel|el, ...props) computed styles as an object
// text(sel|el) trimmed textContent
// settle(ms) await a repaint/timeout
//
// Examples:
// node harness/probe.mjs issues 'return $$(".sec-row").length'
// node harness/probe.mjs 'prs~open106' 'return box(".det-title")'
// node harness/probe.mjs changes 'return css(".dc-file", "color", "font-size")' --theme=light
// node harness/probe.mjs code 'return $$("button").filter(b=>!b.textContent.trim()&&!b.title).length'

import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));
const PAGE = process.env.GS_HARNESS_PAGE
? resolve(process.env.GS_HARNESS_PAGE, "harness.html")
: resolve(HERE, "page/harness.html");
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";

const argv = process.argv.slice(2);
const flags = Object.fromEntries(
argv.filter((a) => a.startsWith("--")).map((a) => {
// Split on the FIRST "=" only: --extra=staging=checkboxes is one flag whose
// value is itself a query fragment.
const raw = a.slice(2);
const at = raw.indexOf("=");
return at < 0 ? [raw, "true"] : [raw.slice(0, at), raw.slice(at + 1)];
}),
);
const positional = argv.filter((a) => !a.startsWith("--"));
const scene = positional[0];
const body = positional[1];

if (!scene || !body) {
console.error("usage: node harness/probe.mjs '<scene>' '<js body>' [--theme=light] [--width=N]");
process.exit(2);
}
if (!existsSync(PAGE)) {
console.error("harness/page is not built — run: node esbuild.js && harness/gen.sh");
process.exit(2);
}

const theme = flags.theme ?? "dark";
const width = Number(flags.width ?? 1600);
const height = Number(flags.height ?? 1000);

// The helpers are prepended to the probe body so every caller has the same
// vocabulary; keeping them here (not in the shim) means the shim stays the
// scene DRIVER and this file owns the inspection language.
const PRELUDE = `
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => [...(r || document).querySelectorAll(s)];
const _el = (x) => (typeof x === "string" ? $(x) : x);
const box = (x) => {
const n = _el(x); if (!n) return null;
const r = n.getBoundingClientRect();
return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width),
h: Math.round(r.height), right: Math.round(r.right), bottom: Math.round(r.bottom) };
};
const css = (x, ...props) => {
const n = _el(x); if (!n) return null;
const s = getComputedStyle(n); const o = {};
for (const p of props) o[p] = s.getPropertyValue(p) || s[p];
return o;
};
const text = (x) => { const n = _el(x); return n ? (n.textContent || "").trim() : null; };
const settle = (ms = 200) => new Promise((r) => setTimeout(r, ms));
`;

const probe = encodeURIComponent(PRELUDE + "\n" + body);
// --extra=k=v[&k=v] appends the shim's own scene switches (staging=checkboxes,
// many=1, ask=1) so a mode reachable only through a pref can still be measured.
const extra = flags.extra ? `&${flags.extra}` : "";
const url = `file://${PAGE}?scene=${scene}&theme=${theme}&probe=${probe}${extra}`;

execFile(
CHROME,
[
"--headless",
"--disable-gpu",
"--hide-scrollbars",
`--window-size=${width},${height}`,
"--virtual-time-budget=12000",
"--dump-dom",
url,
],
// Same guard check.mjs carries: a page that never lets virtual time run out
// hangs headless Chrome forever, and a probe that never returns is worse than
// one that fails — it hangs whatever asked the question.
{ maxBuffer: 64 * 1024 * 1024, timeout: 90_000, killSignal: "SIGKILL" },
(err, stdout) => {
if (err && !stdout) {
console.error("chrome failed:", err.message);
process.exit(1);
}
const m = /<title>PROBE ([\s\S]*?)<\/title>/.exec(stdout);
if (!m) {
const t = /<title>([\s\S]*?)<\/title>/.exec(stdout);
console.error(`no probe result (title was ${JSON.stringify(t?.[1] ?? "")})`);
process.exit(1);
}
const decoded = m[1]
.replace(/&quot;/g, '"')
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&#39;/g, "'")
.replace(/&amp;/g, "&");
try {
const v = JSON.parse(decoded);
if (v && typeof v === "object" && v.error) {
console.error(v.error);
process.exit(1);
}
console.log(JSON.stringify(v, null, 2));
} catch {
console.log(decoded);
}
},
);
Loading
Loading