feat(web): open the preview when an action with previewUrl runs - #142
feat(web): open the preview when an action with previewUrl runs#142alex-woodhouse wants to merge 117 commits into
Conversation
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* ci: stabilize fork nightly tests * ci: isolate image compression test --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(ci): allow Apple notarization to finish * fix(ci): avoid duplicate Apple notarizations --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* ci: build unsigned Windows x64 in fork nightly * ci: add dry_run input to test fork nightly --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
- Run fork CI and nightly jobs for yngatech/t3code Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fix(branding): use yngatech fork identity Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* ci: migrate Ubuntu jobs to Blacksmith * ci: keep lightweight jobs on GitHub runners --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(web): show GitHub service outages * fix(web): clarify GitHub status link affordance * feat(web): make GitHub outage alerts opt-in * refactor(web): remove GitHub status preview override * test(desktop): include GitHub alert setting --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(web): show unsent drafts in the sidebar - Add a draft indicator to sidebar thread rows - Surface drafts containing text or attachments, excluding settings-only state * fix(web): brighten the sidebar draft pencil on row hover The pencil sat at a flat secondary-label colour, so on a settled row it read brighter than the row's own title and stayed put while the rest of the row lifted. Match the PR badge's settled treatment: muted at rest, transitioning with the row on hover. The pencil has no state colour of its own, so it lifts to the same foreground the title does, and the active row opts out like the PR badge already does. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* assets: apply yngatech brand colorway * style(branding): remove redundant palette comments --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
> [!NOTE] > The repo is public, so standard GitHub-hosted runners are free. Moves the two > jobs that gain least from paid hardware onto free runners and right-sizes a > third. Runner changes only — follow-ups tracked at the bottom. ## Problem An audit of the last 8 days of run history (all GitHub retains) put Blacksmith at roughly $176/mo, with Fork CI the largest bucket at ~$80/mo. Two jobs were paying for cores they could not use: - **Fork Test** spends 209s of its 248s in the `Test` step. Only the first ~30s is parallel transform/import fan-out at 100% CPU; the remaining ~2.5min at 15-20% is `apps/server` alone (`transform 4.85s, import 75s, tests 211s` — await-bound tests pinning one or two workers while every other package has finished). It is long-poled, not short of cores, so slower cores cost far less than proportionally. - **Nightly Windows x64** was the least CPU-bound of the three desktop targets per the #51 metrics review. - **Fork Check** peaks at 4-5 cores during Typecheck; only ~54s of its 97s is CPU work at all. ## Fix - Fork Test → `ubuntu-24.04`, nightly Windows → `windows-2025` (free). - Fork Check → `blacksmith-4vcpu-ubuntu-2404`. macOS deliberately stays on Blacksmith — it is genuinely CPU-bound (P95 ≥ 91%) and paces the whole nightly. Expected cost after this lands: ~$176/mo → ~$90/mo, leaving macOS (~$29/mo) as the largest Blacksmith item. ## Verification `actionlint` reports no new findings, both workflows parse, and the desktop build matrix script still emits the expected runner for each target (`macos-arm64` → `blacksmith-6vcpu-macos-15`, `linux-x64` → `blacksmith-8vcpu-ubuntu-2404`, `windows-x64` → `windows-2025`). Note this PR's own checks run under the **old** config; the new runner choices are not exercised until it lands on main. ## Risks Two things only a real run can confirm: disk headroom for the NSIS build on the GitHub-hosted Windows runner, and Fork Test's actual wall time on 4 cores (predicted ~250s → ~300s, since only the 30s phase scales with cores). ## Follow-ups Split out of this PR to keep it to one concern: 1. **`MessagesTimeline.test.tsx` silently skips itself under load.** It defers `await import("./MessagesTimeline")` into `beforeAll(..., 30_000)`. In isolation the file runs in 3s, but under full-suite contention that hook hits its timeout and the file reports `18 tests | 18 skipped` at 30100ms *without failing*. Moving the DOM stubs to `vi.hoisted` fixes it structurally. This is pre-existing, but the slower runner in this PR makes it more likely — worth landing soon after. 2. **Drop the `merge_group` trigger**, now that the merge queue is retired. --- Written by an agent (Claude Code, claude-opus-5).
> [!NOTE] > The fork's teal stage artwork was scoped to the T3 Chat theme, but the default T3 Code theme has no `data-theme-id` for that selector to match. Everyone on the default theme saw upstream's palette. This scopes the fork pigments to the absence of `data-theme-id` and covers the Dev blueprint as well as the Nightly sky. `#96` moved the Yngatech colorway out of `index.css` into `yngatech.css` and scoped it to `html[data-theme-id="t3-chat"] .stage-nightly`. The default T3 Code theme never carries a `data-theme-id`: `applyThemePalette` deletes the attribute whenever the preference does not resolve to a stored palette, and the settings grid renders that state as the T3 Code card. So the one theme most users run could not match the fork's own selector, and fell through to upstream's pigments — the purple Nightly sky and the blue Dev blueprint. Scope the fork pigments to the absence of `data-theme-id` instead, and cover both artworks. The pigments sit on the root rather than on the artwork element because the sidebar focus rings offset against `--stage-art-bottom` from outside the SVG, and would otherwise stay upstream blue while the art went teal. T3 Chat now derives its sky from its own pigments like Grove, Ocean, Ember, and Iris. The fork identity belongs to the fork's default theme, not to a named accent theme. ## Before and after Both captures use the dark default T3 Code theme at the same sidebar width and crop. | | Before | After | | --- | --- | --- | | Nightly |  |  | | Dev |  |  | ## Verification - `vp test run src/components/SidebarStageBackdrop.test.tsx --project unit` — 8 tests passed - `vp fmt apps/web/src/yngatech.css --check` - Computed custom properties in a running dev client: the default theme resolves `--stage-art-top` / `--stage-night-top` to `#5cd6ce` / `#10484f` in light and `#35939c` / `#10484f` in dark; Iris and T3 Chat still resolve to their own OKLCH `color-mix` values, confirming the named built-ins are untouched - Captures above taken from that same client, forcing each stage variant in turn --- Written by an agent (T3 Code, claude-opus-5).
## Problem The merge queue was retired because it duplicated CI work, but Fork CI still carries its plumbing: a `merge_group` trigger and `github.event.merge_group.*` fallbacks in the `changes` job's SHA resolution. Nothing fires it, so it is dead config that implies a workflow the repo no longer uses. ## Fix Remove the trigger and the two fallbacks. `pull_request` and `push` events already resolve the same SHAs on their own: - `BASE_SHA` → `pull_request.base.sha` or `github.event.before` - `HEAD_SHA` → `pull_request.head.sha` or `github.sha` ## Verification Confirmed no merge queue is configured, so nothing is stranded by this: - The `PR + CI` ruleset on `main` contains only `pull_request` and `required_status_checks` rules — no `merge_queue` rule. - All four required checks (`Fork Changes`, `Fork Check`, `Fork Release Smoke`, `Fork Test`) run on `pull_request`, so they still report on every PR. Workflow parses and `actionlint` reports no new findings. Follow-up to #112. --- Written by an agent (Claude Code, claude-opus-5).
…118) ## Problem `MessagesTimeline.test.tsx` needs DOM globals stubbed before its module graph is evaluated, because the web `unit` project runs on the **node** environment. It did that by deferring the import into a hook: ```ts let MessagesTimeline: typeof import("./MessagesTimeline").MessagesTimeline; beforeAll(async () => { vi.stubGlobal("window", { /* ... */ }); ({ MessagesTimeline } = await import("./MessagesTimeline")); }, 30_000); ``` That puts the module graph's transform cost *inside a hook timeout*. Run alone the file takes 3s, but under full-suite contention the hook hit its 30s timeout and the file reported `18 tests | 18 skipped` at 30100ms — **without failing**. CI stayed green having run 18 fewer tests than it appeared to. This is pre-existing, but #112 moved Fork Test to a 4-core free runner, which makes the contention that triggers it more likely. ## Fix Move the stubs into `vi.hoisted`, which runs ahead of imports, so `MessagesTimeline` is imported statically and no hook timeout wraps the module graph. This removes the failure mode structurally rather than by raising the timeout. `vi.hoisted` is already the idiom in ~10 other web test files. ## Verification - File alone: 18/18 pass. - Timing confirms the cost moved out of the hook phase: `tests 2.62s / import 619ms` before, `tests 98ms / import 2.16s` after. No hook timeout applies to the import phase. - Full `@t3tools/web` suite: 250 files / 2451 tests, **0 skipped**, exit 0. Previously this run showed MessagesTimeline at `18 tests | 18 skipped`. - `@t3tools/web` typecheck clean. Worth noting the original stall was intermittent (1 of 2 full runs), so a green run is not by itself proof. The argument for the fix is structural: there is no longer a timed hook around the import. --- Written by an agent (Claude Code, claude-opus-5).
The fork's scheduled release pipeline currently depends on Blacksmith capacity even though startup latency does not matter for this workload. During Blacksmith's August 13 us-west incident, the first nightly job waited roughly 40 minutes for a runner. Move Fork Nightly and the reusable desktop build to standard GitHub-hosted runners, which are free for this public repository. Fork CI's quick rebase check also moves to GitHub; the CPU-heavy check retains Blacksmith for pull requests but uses GitHub on pushes, preserving fast review feedback without paying for non-PR work. Verification: `actionlint` passes for all three changed workflows, and `git diff --check` is clean. --- Written by an agent (T3 Code, gpt-5.6-sol).
> [!NOTE] > Extraction is cached per pull request and runs in sequential batches, so a nightly re-reads only > what is actually new and no diff is dropped to fit the prompt budget. Changelogs now generate with > `gpt-5.6-terra`, which followed the style guidance more faithfully than `gpt-5.6-sol` under repeated > sampling while completing every run, and costs less. ## Problem Extraction reads every commit the fork carries, and it ran in full on all five nightly runs a day. With 94 commits on the stack the evidence measured ~368k characters against a 160k budget, so `fitEvidenceToPromptBudget` zeroed the largest diffs first: 68 of 94 changes reached the model with no code at all, every run, and it worsened with each merge. A single whole-stack call also sat against the schema's 120-record ceiling, where overflow drops changes with no error to report. ## Fix Cache extraction results per pull request and send only new evidence to the model. - **Key.** A hash of the evidence the model reads — PR title, body, files — with `git patch-id` standing in for the diff, since the nightly rebase moves context lines and hunk headers while the patch ID survives them. Combined with a hash of the extraction request itself, so editing the prompt, schema, model, reasoning effort, output limits, or diff budget invalidates entries without anyone bumping a version. - **Batching.** Pending evidence is split into chronological batches sized by the records a response may hold, not by prompt length. Batches run in order rather than concurrently so each is given the capability names every earlier change already used, which is what the single call used to provide. The cache is written after each batch, so a failed batch resumes there next run. - **Grounding.** With batching, `MAX_DIFF_LENGTH` rises from 4k to 10k. Evidence read without its diff is left uncached, so a later batch reads it again whole. - **Identity changes.** Extraction now flags changes that only alter how this build identifies itself. They stay in release highlights, which announce what shipped, and leave the rolling issue, which answers how the fork differs from upstream. The release commit list is rendered from git and still lists them. - **Voice.** Style guidance gained the missing bans and worked examples: no padding adjectives, no repository or release-channel vocabulary, and bug fixes named by the symptom a user would report. ## Model and limits Repeated sampling compared `gpt-5.6-sol`, `gpt-5.6-luna`, and `gpt-5.6-terra`. On a focused probe the three were indistinguishable at classifying identity changes (recall 7.6, 7.8, 7.6 of 8; no false positives). Synthesising an identical 50-record set five times each separated them: | model | style examples reproduced verbatim (of 11) | runs completed | | --- | --- | --- | | `gpt-5.6-terra` | 8, 9, 7, 8, 7 | 5/5 | | `gpt-5.6-sol` | 4, 6, 8 | 3/5 | | `gpt-5.6-luna` | 4, 4, 5, 6 | 4/5 | Terra becomes the default: better guidance adherence, no failures, lower cost. `luna` reproduced one example in none of its runs. Sol's two failures were real limits rather than model quirks, and both are fixed here: - Reasoning tokens count against `max_output_tokens`, and synthesis reasons over every record it reconciles. A rolling summary truncated mid-JSON at 4000 and again at 8000, so synthesis now gets 16000 while extraction keeps 8000. - A truncated response still carries well-formed output text, so it used to fail later as `SyntaxError: Expected ',' or ']'`. The response's `incomplete` status is now checked and named. - Synthesis over a full record set has run past two minutes, so the request timeout rises to four. ## Verification `vp test run scripts/generate-fork-features-summary.test.ts` (27 passed), scripts typecheck, targeted lint. Behaviour was checked end to end against the real stack with a local stub for the model endpoint: | scenario | result | | --- | --- | | cold cache | batches with `0 left uncached` throughout — no diff dropped anywhere | | unchanged repository | `0 of 95 extracted, 95 reused`, byte-identical output | | five new merges | `5 of 94 extracted, 89 reused` | | full stack rewritten by rebase | `3 of 95 extracted, 92 reused`; the 3 are commits with no PR number, keyed by SHA | | forced failure on batch 3 | run failed with batches 1–2 durable; next run `55 of 95, 40 reused` | Real generations confirmed voice and the identity split — 8 of 57 records came back flagged as identity, none reached the rolling summary, and the branding work appeared in release highlights — and a final cold run on terra completed all six batches with no degraded evidence. --- Written by an agent (Claude Code, claude-opus-5).
> [!NOTE] > Cloning a GitHub fork now wires up the parent as `upstream` and pins which repository `gh` targets, with the choice offered during the clone and editable afterwards. Cloning a fork left the parent repository unreachable — no `upstream` remote — and, worse, `gh` resolves a two-remote checkout to the fork's parent, so a pull request created from the workspace could land on someone else's project without ever saying so. A fork clone now adds `upstream`, fetches it the way `gh repo clone` leaves it, and pins a default repository by writing the same `remote.<name>.gh-resolved` config `gh repo set-default` uses, so T3 Code and the GitHub CLI always agree. Because two remotes mean two possible targets, the clone flow asks which one; the cloned fork leads, so the pin agrees with the remote a branch on the fork tracks, and the parent stays one keystroke away for contributing upstream. | Before | After | | --- | --- | |  |  | The destination step then states the relationship instead of repeating the repository name as a URL:  | Before | After | | --- | --- | |  |  | Mobile gets the same choice at clone time, plus a **Default repository** row in the thread git sheet so the phone is not a one-way door: <img src="https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/mobile-after.png?v=064d1d3f9" width="320" alt="The fork chooser on iOS with owner avatars"> - `gh-resolved` is not always `base`: with a single remote, `gh repo set-default` writes an `owner/repo`. That value is parsed and shown rather than silently overwritten, and the parse is now shared with `RepositoryIdentityResolver` so the two cannot drift. - The pin is written with `--replace-all`, since `gh` *adds* resolutions and a plain write refuses to overwrite a multi-value key (exit 5). - Only forks send `provider`/`repository` on clone, so every other clone costs no extra `gh repo view` and behaves exactly as before against an older server. - Identity still follows the branch's tracked remote first, per #79; the pin only decides when a branch has no upstream yet. - `vp test run apps/server/src/sourceControl apps/server/src/project packages/shared/src/git.test.ts packages/client-runtime` — 797 tests passed, including new cases for the `owner/repo` pin, the unset path, dotted remote names, and a fetch failure leaving the clone intact - Targeted typecheck, lint, and formatting across server, contracts, shared, client-runtime, web, and mobile - Web: clone flow and Settings driven by hand and captured above - iOS Simulator: fork chooser verified on device; the git sheet row is typechecked but unexercised, since it needs a thread with real git state - Two independent agent reviews; findings folded in --- Written by an agent (Claude Code, claude-opus-5).
The destination and local-folder steps of Add Project had no navigation title, so the sheet header showed their route names: `AddProjectDestination` and `AddProjectLocal`. The repository step already sets one, so this just applies the same pattern to its two siblings. Titles are now **Destination** and **Local Folder**. Split out of #123, where the raw route name showed up in a screenshot. ### Verification - `tsc --noEmit` for `apps/mobile`, targeted lint and formatting - iOS Simulator: the destination sheet header reads "Destination" over Fast Refresh --- Written by an agent (Claude Code, claude-opus-5).
> [!NOTE] > TL;DR: Keep PR badges and merged/closed thread settlement stable when a pruning fetch removes the hosted branch ref. Git status treated a missing remote-tracking ref as proof that a local branch had never been published. When a host deleted a merged branch and fetch pruning removed that ref, T3 stopped querying the provider, dropped the PR badge, and could move the thread back into the active list. Use the surviving branch remote and merge configuration to recover the hosted head branch after pruning. Local-only and genuinely unpublished branches retain the zero-provider-call fast path, while config-read failures fall back to querying instead of caching a false no-PR result. ## Verification - vp test run apps/server/src/git/GitManager.test.ts (85 passed) - vp lint apps/server/src/git/GitManager.ts apps/server/src/git/GitManager.test.ts - vp run t3#typecheck - Independent reviewer follow-up: approved with no blocking findings --- Written by an agent (T3 Code, gpt-5.6-sol).
Every conflict-resolved `source_ref` reshapes the patches it touched, so the stale-stack guard from #95 flags them and `allow_missing_main_patches` has to be set on essentially every dispatch — and that boolean waives the *entire* missing list, including a patch that merged to `main` after the review. The blanket override defeats the guard in exactly the window it was built for (#94). This replaces the boolean with `waived_main_patches`: the reviewed commits themselves, space or comma separated. Waived patches are listed in the log and skipped; any other missing patch still fails the dispatch, so a PR that lands on `main` between review and re-dispatch is caught instead of silently waived. A literal `true` is rejected with a pointer to the new input, a waiver entry that does not resolve fails loudly, and waivers that turn out to be unnecessary are noted without failing. The runbook documents the new flow. Verification: - `vp test run scripts/check-source-stack.test.ts` (12 passed) - `vp lint scripts/check-source-stack.test.ts` - `vp fmt --check scripts/check-source-stack.test.ts docs/operations/fork-nightly.md .github/workflows/fork-nightly.yml` - `bash -n .github/scripts/check-source-stack.sh` - `shellcheck .github/scripts/check-source-stack.sh` - `actionlint -ignore 'label ".*" is unknown' .github/workflows/fork-nightly.yml` --- Written by an agent (T3 Code, claude-fable-5).
When `main` gains a commit while a nightly run is in flight, promotion is skipped and the next candidate picks it up. That is right for scheduled runs but wrong for `source_ref` dispatches: the run existed to promote the resolved stack, and the skip is a single log line in a green run — the maintainer finds out hours later when the next scheduled rebase hits the same conflicts again. This makes both promotion paths (the release job's promote step and prepare's align step) fail loudly when `main` moved during a `source_ref` run, so the existing `notify_failure` Discord alert fires while the resolution branch is still warm to refresh and re-dispatch. An already published release stands either way; scheduled runs keep the benign skip. The runbook documents the behavior. Verification: - `actionlint -ignore 'label ".*" is unknown' .github/workflows/fork-nightly.yml` - `vp fmt --check docs/operations/fork-nightly.md .github/workflows/fork-nightly.yml` - `git diff --check` --- Written by an agent (T3 Code, claude-fable-5).
> [!NOTE] > Tool activity and markdown file chips now omit generated worktree directory names from paths that are already scoped to the active thread. File-change rows were formatting paths relative to the active workspace, then adding the workspace basename back. Generated worktrees therefore exposed labels such as `t3code-1fc35d1b/` even though the thread already supplied that context. Markdown file chips repeated the same label in tooltips and duplicate-name disambiguation. This moves workspace path formatting into a shared, cross-client helper and lets scoped UI omit the workspace label. Web, desktop, and mobile tool rows now show repository-relative paths; expanded details and copied relative paths follow the same rule. Absolute paths outside the workspace remain unchanged, and file-opening targets still use the full resolved path. ## Before and after | Before | After | | --- | --- | |  |  | ## Verification - `vp test run apps/web/src/markdown-links.test.ts apps/web/src/filePathDisplay.test.ts apps/web/src/components/chat/MessagesTimeline.test.tsx apps/mobile/src/lib/threadActivity.test.ts packages/shared/src/toolRowPresentation.test.ts` — 89 tests passed - Web and shared typechecks passed - Targeted lint, formatting, and `git diff --check` passed - Verified the seeded Edited file row in the authenticated T3 preview; preview navigation and DOM inspection passed - Mobile activity tests passed. The package-wide mobile typecheck is currently blocked on `origin/main` by three untouched `showsSearchDismissButton` type errors in `HomeHeader.tsx`, `NewTaskContextPickerScreens.tsx`, and `ThreadSettingsSheet.tsx`. --- Written by an agent (T3 Code, gpt-5.6-sol).
Background update downloads landed in #110, but the sidebar control still treated downloading as a call to action: it shared the `isUpdateState` styling branch with the states that want a click, so an automatic download lit the button and its changelog tooltip in update blue while needing no input at all. The downloaded state then offered a circular-arrow icon — the same metaphor this app uses for refresh and retry everywhere else, including the check-for-updates state of this very button. This splits the tone decision into `resolveDesktopUpdateButtonTone` (`"cta" | "quiet" | "idle"`), so call-to-action colour is reserved for states that actually want a click: install, or retry a failed download. The intended flow is now visually staged: 1. Check for updates in the background (existing pollers, unchanged) 2. Update available → download starts automatically (#110, unchanged) 3. **Downloading → muted progress ring around a download glyph, no CTA colour** 4. **Downloaded → CTA colour with an upward arrow, matching the update glyph in provider settings** | | Downloading | Downloaded — ready to restart | | --- | --- | --- | | Before |  |  | | After |  |  | Full motion — indeterminate spin before the first progress event, gliding through the 10% progress broadcasts, then flipping to the CTA on completion: https://github.com/user-attachments/assets/80d64f3c-06d3-4f9e-aace-bfa2e29f1be8 Implementation notes: - The ring follows the `ContextWindowMeter` construct (track + dash-offset arc, `-rotate-90 transform-gpu`, `motion-reduce:transition-none`), with its stroke transitioned over 500ms so the 10%-step broadcasts from the main process glide instead of jumping. - `reduceDesktopUpdateStateOnDownloadStart` reports `downloadPercent: 0` until the first progress event, which would draw a zero-length arc, so `0` renders as an indeterminate spinning arc instead. - A manual retry keeps the button natively disabled for the whole download; the quiet branch omits the `disabled:` dimming so background and manual downloads render identically. - The changelog tooltip stays available while downloading, just without the update-tinted glass treatment. - Tone is decided in `desktopUpdate.logic.ts` and covered by tests, so a regression to the always-CTA behaviour would now fail the suite. Tests: - `vp test run apps/web/src/components/desktopUpdate.logic.test.ts apps/web/src/state/desktopUpdate.test.ts apps/web/src/components/desktopUpdate.toast.test.tsx apps/desktop/src/updates/DesktopUpdates.test.ts` - `vp run --filter @t3tools/web typecheck` - Targeted lint for the changed TypeScript files --- Written by an agent (T3 Code, claude-fable-5).
> [!NOTE] > TL;DR: Run the serial server suite and the remaining workspace suites on separate runners so their CI time overlaps. The Fork Test job currently waits for every dependency package suite before starting the server suite. Since server test files deliberately run serially, those two long phases add together on the critical path. This splits the job into server and workspace jobs. The workspace job explicitly selects all 13 non-server test packages and retains the isolated image-compression test, Electron setup, and Rust resource-monitor tests. Verification: - actionlint .github/workflows/fork-ci.yml - Confirmed the workspace selector resolves to exactly 13 non-server packages - Confirmed the server selector resolves only to apps/server --- Written by an agent (T3 Code, gpt-5.6-sol).
> [!NOTE] > Fixes a regression upstream where loading pull request details exhausted the GitHub GraphQL API quota because reactions were queried beneath two 100-item connections, making each refresh cost 104 points. Opening a pull request detail view could exhaust GitHub’s 5,000-point hourly GraphQL quota because review-comment reactions sat beneath two 100-wide connections. The activity query was refreshed once per minute while the view remained active. Narrow the initial review-thread query to 25 threads and 10 comments, while retaining pagination and the existing 1,000-thread traversal ceiling. Add a regression assertion that caps the nested connection fan-out. The resulting live query costs 6 GraphQL points. Tests: - `vp test run apps/server/src/pullRequest/gitHubPullRequestJson.test.ts apps/server/src/pullRequest/GitHubPullRequestCli.test.ts` - `vp run --filter t3 typecheck` - targeted `vp lint` on the four changed files --- Written by an agent (T3 Code, gpt-5.6-sol).
TSX diffs can render without syntax colors on their first pass. With the current Shiki JavaScript regex engine, a fresh TSX `codeToTokens` call returns broad default-color tokens; a second call on the same highlighter produces the expected tokens. The engine emits no warning or error. Disabling its lazy-regex compilation also fixes the first pass. Configure Pierre's diff workers to use Shiki's Oniguruma/WASM highlighter, which tokenizes TSX correctly on the first pass. This is a focused workaround for the first-render engine behavior rather than a claim that the JavaScript engine generally lacks TSX support. Testing: targeted web lint, `vp run typecheck`, and an isolated first-pass TSX tokenization comparison across the JavaScript and WASM engines. --- Written by an agent (T3 Code, gpt-5.6-sol).
Fork Check still used Blacksmith for pull-request runs even though the server and workspace suites determine normal full-CI completion time. Run Fork Check on GitHub-hosted Ubuntu for every trigger, removing the final Blacksmith dependency from enabled fork workflows. --- Written by an agent (T3 Code, gpt-5.6-terra).
GitHub outage alerts have been opt-in since the feature was introduced, so users can miss useful context when GitHub operations fail. Default the alerts to enabled when no preference has been stored, while continuing to respect an explicit opt-out. Update the focused contract coverage and user documentation to match. Tested with `vp test run packages/contracts/src/settings.test.ts` (37 tests passed). --- Written by an agent (T3 Code, gpt-5.6-sol).
> [!NOTE] > `#123`, `GH-123` and `owner/repo#123` in a pull request body are now links, the way they are on > GitHub. What each number turns out to be decides where it opens: a pull request in a project on > this machine opens as a tab beside the one being read, an issue opens in a browser, and a number > the host has nothing under is underlined in red and still opens. GitHub's autolinked references are its own extension rather than GFM, so remark left them as they were: every `pingdotgg#6039` an agent or a reviewer wrote in a description, a comment or a review stayed plain text, and following one meant retyping it into a browser. Reading them is the easy half. Knowing what one *is* takes asking the host, and a body can name a dozen — so they are asked about together, in one GraphQL document, aliased by repository and by number. `issueOrPullRequest` answers which of the two each number turned out to be, which is what decides where it opens; a cache keeps a panel of many bodies from spawning a process per body for numbers a neighbour just resolved. The link is addressed at `/issues/{n}` before any of that comes back, because the host redirects that to `/pull/{n}` for a pull request — so it is already right in a browser, and resolving only ever improves it. A reference clicked before its answer arrives follows it as written. Everything else follows from one rule: **only an answer marks a link as broken.** A request that failed on the way — rate limited, logged out, offline — leaves every reference exactly as it was, and a null is read as nothing-there only where the host filed `NOT_FOUND` against it, since `FORBIDDEN` is SAML or an IP allowlist standing between the reader and something they can very likely open themselves. A host having a bad minute must not repaint a body full of good references as mistakes. Inert unless a surface passes the repository to read numbers against, so `#2` in a conversation — where it is far more likely to be a step than an issue — stays the plain text it reads as. The description of #126, which cites the stale-stack guard from `#95` and the window it was built for (`#94`). | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/before.png" alt="Reference numbers rendered as plain grey text" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/after.png" alt="The same numbers rendered as links" width="460"> | `#95` resolves to a pull request and opens as a tab here; `#94` resolves to an issue and opens in a browser, since there is no issue surface to open it in. A reference the host has nothing under keeps its link colour and takes the mark an unknown word takes. No pull request in this repository happens to cite a number the repository lacks, so the state below was forced on a resolved reference to photograph it: <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/missing.png" alt="A reference underlined with a red wavy line" width="460"> Three layers, each of which stands alone: the plugin that reads references and renders nothing differently on its own, the request that answers them and is called by nobody, then the wiring that turns both on. - `vp test run` for the touched suites: `sourceControl` (147), `GitManager`, and the web `markdown-github-references`, `chat` and `pullRequest` suites (469). - Targeted `tsgo --noEmit` for `apps/web`, `apps/server` and `packages/contracts`, at each of the three commits rather than only at the tip. - The matching rules are GitHub's own, each checked against its renderer before being written down: `foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and `/#123` are, `#0` is not, and code spans and link labels are left alone. - Read live against this repository in `test-t3-app`: references linkified in a real description, `#95` re-addressed itself to `/pull/95` and opened here, `#94` stayed addressed for the browser. That pass is also what found two faults nothing else did — a memoized renderer holding the first render's answers, and a mark cancelled by `.chat-markdown a { text-decoration: none }`. --- Written by an agent (T3 Code, claude-opus-5).
The Files tab could only expand or collapse folders one at a time, even though diff views already provide a bulk control. This adds the matching collapse/expand button beside refresh. It follows manual tree expansion state, collapses every directory when any are open, and expands every directory when the tree is fully collapsed. ## Verification - `vp run --filter @t3tools/web typecheck` - Files-focused web unit suite: 32 tests passed - Authenticated local web pass: collapse moved the visible tree from 5 expanded folders to 0 and changed the action to “Expand all files”; expanding restored the folders and changed it back to “Collapse all files” --- Written by an agent (T3 Code, gpt-5.6-sol).
> [!NOTE] > Moves pull request checks out of the Summary scroll and into a first-class Checks tab, while preserving the existing check actions and header rollup. ## What Changed - Added a dedicated Checks tab between Summary and Timeline in both full and condensed pull request chrome. - Moved the existing check list, host links, status labels, and failing-check Fix handoff into the new tab. - Made the tab-bar check rollup a flat button that opens Checks, and kept the status icon beside the rollup in the tab content. - Added a discoverable empty state and a Checks-specific loading ghost. - Removed the nested Checks section from Summary and added focused component coverage. ## Why Checks are a primary pull request state, but they were buried below the description in Summary. Giving them a tab makes the result directly reachable without making users scroll through unrelated content. ## UI Changes | Before: nested under Summary | After: dedicated Checks tab | | --- | --- | |  |  | Screenshots use the Pull Requests page with host-reported checks from repository PRs #137 and #138, respectively. ## Verification - `vp test run apps/web/src/components/pullRequest/PullRequestChecksTab.test.tsx apps/web/src/components/pullRequest/pullRequestChecks.test.tsx` - `vp run --filter @t3tools/web typecheck` - Targeted formatting and lint checks for the changed pull request components - Authenticated local web preview against repository PR #138: the tab-bar rollup opens Checks, which renders all nine host-reported runs with the status icon beside the summary and no extra divider ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for the UI change - [x] No video is needed because this change adds no motion or timing behavior --- Written by an agent (T3 Code, gpt-5.6-sol).
`previewUrl` and `autoOpenPreview` have been persisted, round-tripped through the Actions dialog and the t3.json importer, and then ignored: nothing read them at runtime, so ticking auto-open did nothing. Both ways an action can start now honour them: - Manual runs (toolbar button, actions menu, keybinding) go through `runProjectScript`, which opens the preview after the command reaches the terminal. The write-failure branch becomes an early return so the preview only follows a successful write. - `runOnWorktreeCreate` scripts are launched by the server and never touch `runProjectScript`. No server change was needed: the existing `setup-script.started` activity already carries `scriptId`, so the client looks the action up and opens the preview from that. Keyed by activity id, and finished runs are filtered out, so a reconnect or refetch that replays the activity does not reopen the panel. URLs go through `normalizePreviewUrl`, the same helper the browser URL bar uses, so `localhost:5173` works. A malformed `previewUrl` is swallowed rather than surfaced: the command is already running by then, and a bad URL is not a script failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b0a9983 to
90c3747
Compare
incognitojam
left a comment
There was a problem hiding this comment.
Thanks — the manual-run path here is in good shape, but I'd like to reduce this PR's scope to that path and split the runOnWorktreeCreate path into a follow-up. Two reasons, one small and one structural.
1. The preview URL isn't resolved against the thread's environment (applies to both paths, small fix)
resolveScriptPreviewUrl (apps/web/src/components/preview/openScriptPreview.ts:24) only runs normalizePreviewUrl, so localhost:5173 reaches the preview as-is. For a thread on a remote environment, the script runs on the remote machine while the preview loads the desktop's own localhost — potentially an unrelated local service. The codebase already solves this: resolveDiscoveredServerUrl (apps/web/src/browser/browserTargetResolver.ts:240) rewrites loopback URLs to the environment host, and openDiscoveredPort.ts — the exact precedent this PR's description cites — goes through it. openScriptPreview should resolve with threadRef.environmentId the same way (note openDiscoveredPort records the original URL in history and opens the resolved one — keep that split). The test at openScriptPreview.test.ts:90 currently asserts the unresolved URL, so it locks the bug in and needs updating with the fix.
2. Path B has no durable "already handled" state — several symptoms, one root cause
The dedup for setup-script.started is a per-React-mount ref (apps/web/src/components/ChatView.tsx:3131), and every preview.open unconditionally mints a new server-side session (newPreviewTabId(), apps/server/src/preview/Manager.ts:216). That combination produces a family of problems:
- Duplicates: a second window or client viewing the same thread processes the same activity independently and opens its own preview session; a renderer reload resets the ref and can reopen.
- Reopen-forever: a dev server never emits
setup-script.completed, so its run stays "unfinished" indefinitely — every fresh mount with no active preview tab reopens the panel, including after the user deliberately closed it and restarted the app. - Never-opens: the inverse case — a script that launches a server in the background and exits can have its
startedand terminal activity arrive in one batch, sounfinishedSetupScriptStartsdrops it and no preview opens at all. - No retry: the activity id is added to the handled set (
ChatView.tsx:3136) before the script lookup and beforepreview.openresolves, so a transiently stale project snapshot or a failed RPC is consumed permanently.
I don't think these are four bugs to patch individually. They're one missing design decision: where does "this setup run's auto-open has been consumed" live? The client can't answer that from replayed activities plus in-memory state, no matter how the filtering is tuned. It most likely needs to be server-owned — a consumed marker on the setup run, or the server (which already owns the run lifecycle in ProjectSetupScriptRunner) driving the open — and that's a bigger change than this PR should absorb.
Suggested split
Reduce this PR to Path A plus the fix for issue 1. That path is sound as written: the write-failure early return is behavior-preserving, fire-and-forget is the right call, the opt-outs and malformed-URL swallow are sensible, and the unit tests are good. Path B moves to a follow-up whose PR description leads with the idempotency design, and unfinishedSetupScriptStarts and its tests move with it.
Written by an agent (Claude Code, claude-fable-5).
2698323 to
44e3d33
Compare
What Changed
ProjectScript.previewUrlandProjectScript.autoOpenPreviewnow actually open the in-app browser preview when an action runs. Both paths an action can start from are covered:Path A — manual run (toolbar button, actions menu, keybinding).
runProjectScriptinChatView.tsxopens a terminal, writes the command, and previously returned. It now opens the preview after a successful write. The trailing write-failure block became an early return so the preview only follows a command that actually reached the terminal. The call is deliberately not awaited: a slow or unreachable preview target must never delay the terminal.Path B —
runOnWorktreeCreate. These never reach the client'srunProjectScript— the server runs them inProjectSetupScriptRunner.runForThread. No server change was needed: the existingsetup-script.startedthread activity already carriesscriptId, so the client looks the action up inactiveProject.scriptsand opens the preview from that.Shared logic lives in the new
apps/web/src/components/preview/openScriptPreview.ts, following the precedent of the siblingopenDiscoveredPort.ts(openPreviewSession(...)thenuseRightPanelStore.getState().openBrowser(...)).Behaviour details:
normalizePreviewUrlfrom@t3tools/shared/preview— the same helper the browser URL bar uses — solocalhost:5173works as well as a fully-qualified URL.previewUrlis swallowed and the preview skipped. The command is already running by the time we get there, so a bad URL must not read as a script failure — no toast, no thread error.autoOpenPreview !== trueopts out. ApreviewUrlon its own does not force the panel open; it stays what it already was, a suggestion source for the preview empty state.deriveUnfinishedSetupRuns. A reconnect or refetch that replays a pastsetup-script.starteddoes not reopen the panel.Why
These two fields have been inert since they were introduced. They are validated by the contracts schema, editable in the Actions dialog, round-tripped through the
t3.jsonimporter, and rendered with apreview · desktop onlybadge in project settings — and then never read at runtime. Users configure a preview URL, tick auto-open, and nothing happens.Path B is the case that matters most in practice: the dev server an action starts on worktree creation is exactly the thing you want the preview pointed at, and it is the path that never touched the client at all.
On "desktop only"
The badge and the schema comments say these fields are ignored on web. That turned out not to need a decision: the preview panel is already desktop-only everywhere —
isPreviewSupportedInRuntime()(previewStateStore.ts:453) isBoolean(window.desktopBridge?.preview), andopenFileInPreviewearly-returns on it. The new helper gates on the same check, so the badge and the schema wording stay accurate as written. No docs change.UI Changes
No new UI. An existing panel opens in a case where it previously did not.
Testing
Automated, all from a clean install at this branch:
pnpm typecheck— clean across all 15 packages (Effect LSP suggestions only, all pre-existing).pnpm lint— no errors; no warnings on any touched file.pnpm fmt:check— clean.pnpm --filter @t3tools/web test— 266 files / 2620 tests pass, including 11 new tests inopenScriptPreview.test.tscovering URL normalisation, each opt-out, the malformed-URL swallow, the missing-bridge case, a failed preview session, and the unfinished-run filter.pnpm --filter @t3tools/web build— succeeds.pnpm testwas compared against a stashed baseline and is byte-for-byte identical before and after: the one real failure (scripts/build-desktop-artifact.test.ts > skips the primary native probe for cross-architecture Windows payloads) is pre-existing and unrelated.Manual, on a real desktop (Electron) build with the preview bridge live — all six steps below were executed and passed. Setup: a throwaway git repo added as a project, a static page served on `localhost:5199` with title `PREVIEW-TARGET-OK`, and the action created through the real Actions dialog. Panel state was confirmed out-of-band by listing the Electron webview targets over CDP, so "the panel opened" means the preview webContents actually loaded the target page, not just that a tab appeared.
Two environment notes, neither related to this change. Electron's binary had not downloaded in my sandbox and had to be installed before any of this was possible. And T3 takes the terminal shell from `$SHELL` (`terminal/Manager.ts:470`); my login zsh runs the powerlevel10k configuration wizard on startup, which parks every new terminal at an interactive prompt so no command ever executes. I re-ran the suite with `SHELL=/bin/bash` to get a usable shell. Worth knowing if setup-script behaviour is ever debugged against a zsh with an interactive rc.
Checklist
🤖 Generated with Claude Code