Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
188 commits
Select commit Hold shift + click to select a range
e139768
feat(server): sends can start a fresh provider session (#30323)
juliusmarminge Nov 5, 2026
99e361e
feat: /clear starts a fresh session in the same thread (#30325)
juliusmarminge Nov 5, 2026
4aab23c
feat: scheduled tasks can run in one thread with a fresh session each…
juliusmarminge Nov 5, 2026
37481f0
fix(web): dragging a sidebar thread no longer scrolls the list out fr…
juliusmarminge Nov 5, 2026
e4b98f9
fix(assets): preserve the PR repository for GitHub media (#21631)
juliusmarminge Nov 5, 2026
a09aef3
fix(server): load PR uploads from SSO organizations (#21632)
juliusmarminge Nov 5, 2026
6a4031a
fix(mobile): the Duo inspector stops at its widest instead of vanishi…
juliusmarminge Nov 5, 2026
bc1b558
test(server): launcher PATH lookup test no longer drops the Windows d…
tris203 Nov 5, 2026
ded4dac
test(server): replay tests delete their temp directories (#23445)
only21mil Nov 5, 2026
66633fb
fix(mobile): transcribe Android audio during recording (#31300)
PixPMusic Nov 5, 2026
bf5485c
test(web): linked-environment subagents keep opening in their own thr…
RealDoc06 Nov 5, 2026
0961824
test(server): pin post-commit outbox settle boundary (#27560)
Narayan201120 Nov 5, 2026
132be33
test: remove unreachable github cli decoding test (#28115)
t3-code-bot[bot] Nov 5, 2026
a624256
test(server): retain delegated child events when detaching a parent (…
benjamin-di Nov 5, 2026
d598938
test(lint): pin which ResizeObserver constructions no-raw-resize-obse…
only21mil Nov 5, 2026
9a2ca77
test(mobile): drop two deleted tests from the React test list (#28800)
only21mil Nov 5, 2026
53c26b1
test(web): worked-for expand test waits for the timeline to finish lo…
saphid Nov 5, 2026
0d3b01a
test(web): avoid PDF worker startup timeouts in attachment tests (#29…
voltcrash Nov 5, 2026
e402c94
test(web): source control writing settings test waits for the reset b…
juliusmarminge Nov 5, 2026
6bd73c3
test(web): keep the work heading test inside the viewport (#30613)
voltcrash Nov 5, 2026
95c0fbd
test: use an empty config file instead of the null device for git fix…
Pyseph Nov 5, 2026
c72de52
test: remove stale tests for unused thread and onboarding helpers (#3…
t3-code-bot[bot] Nov 5, 2026
268e581
test(server): correct Windows path and settings assertions (#30983)
ElliotDrel Nov 5, 2026
2d0efd3
feat(claude): Ultracode works with any effort, like Claude Desktop (#…
Bil0000 Nov 5, 2026
0ea3c02
perf(pr): pull requests paint from data already held, on web, desktop…
Bil0000 Nov 5, 2026
e71e4d2
fix(mobile): approval card buttons line up (#31415)
shivamhwp Nov 5, 2026
f7409b0
fix(gitlab): author names link to GitLab profiles (#30418)
TonybynMp4 Nov 5, 2026
938b4d4
fix(gitlab): merge messages drop agent credits (#30441)
TonybynMp4 Nov 5, 2026
25b5329
fix(gitlab): file and image comments appear as review threads (#30425)
TonybynMp4 Nov 5, 2026
c0cb75e
fix(web): screen readers no longer read out a secret typed into the r…
juliusmarminge Nov 5, 2026
e1494ae
perf(pr): actions update every client from the host's reply instead o…
Bil0000 Nov 5, 2026
0f28f6f
feat(mobile): start side chats with /side (#31709)
juliusmarminge Nov 5, 2026
4bfb896
feat(claude): expose Ultracode on Claude Sonnet 5.5 and Haiku 5.5 (#3…
DrSeedon Nov 5, 2026
4a94658
feat(settings): Simlock, Antigravity and OpenCode credentials can com…
juliusmarminge Nov 5, 2026
0e233fc
fix(mobile): every inspector pane draws its bar through AppBar
juliusmarminge Nov 5, 2026
da38f86
refactor(mobile): routes declare page, sheet or modal and presets own…
juliusmarminge Nov 5, 2026
8897f1e
refactor(mobile): features stop building navigators for their bars
juliusmarminge Nov 5, 2026
162f81f
refactor(mobile): every screen draws one AppBar or SheetHeader, on bo…
juliusmarminge Nov 5, 2026
faf9d73
refactor(mobile): the workspace shell lives in layout and draws the l…
juliusmarminge Nov 5, 2026
d694c44
fix(server): a saved source control token that can't be read no longe…
juliusmarminge Nov 5, 2026
7462b16
feat(secrets): answer a secret request with a secret provider referen…
juliusmarminge Nov 5, 2026
187781e
fix(settings): removing a secret provider clears the fields that used…
juliusmarminge Nov 5, 2026
b46abbf
docs(user): explain secret providers and secret requests (#31654)
juliusmarminge Nov 5, 2026
b8dce83
feat(server): transcribe audio on a chosen environment (#30625)
juliusmarminge Nov 5, 2026
a9a1b8c
feat(web): opt into environment dictation on this device (#30623)
juliusmarminge Nov 5, 2026
aabd562
feat(desktop): dictate into browser annotations with the same consent…
juliusmarminge Nov 5, 2026
79ee39f
feat(mobile): opt in to dictation on a paired environment (#31059)
juliusmarminge Nov 5, 2026
fb9f1c6
fix(web): keep the folder path caret in place while editing (#31642)
juliusmarminge Nov 6, 2026
6b53b3d
perf(mobile): call the highlights lexer through Nitro, up to 10x fast…
camchis Nov 6, 2026
43cf364
fix(claude): turns use the downloaded model catalog (#31717)
juliusmarminge Nov 6, 2026
e945fcc
fix(gitlab): bot authors are flagged as bots (#30383)
TonybynMp4 Nov 6, 2026
444921e
fix(gitlab): merge requests can edit their labels (#30483)
TonybynMp4 Nov 6, 2026
2917f28
perf(mobile): faster thread sync, connects, and reconnects on bad wif…
Bil0000 Nov 6, 2026
3478e56
fix(mobile): iPad composer drops when the keyboard floats or minimize…
juliusmarminge Nov 6, 2026
a21f89d
perf(mobile): stop iOS syntax highlighting from looking up a thread-l…
camchis Nov 6, 2026
e136e8e
fix(gitlab): edited comments wake PR watchers (#30403)
TonybynMp4 Nov 6, 2026
b871551
fix(gitlab): merge requests can delete their source branch (#30453)
TonybynMp4 Nov 6, 2026
05bd5a1
fix(gitlab): comments link to their note on GitLab (#30411)
TonybynMp4 Nov 6, 2026
71f251d
fix(mobile): iOS terminal accepts Chinese, Japanese and Korean input …
juliusmarminge Nov 6, 2026
661eae3
fix(mobile): Android app no longer stays offline after reopening (#28…
kahlstrm Nov 6, 2026
81922e6
fix(mobile): show one Android notification per thread completion (#27…
shivamhwp Nov 6, 2026
16f1d0a
fix(mobile): stop Android composer flicker at the wrap boundary (#27682)
shivamhwp Nov 6, 2026
b39d0f2
fix(mobile): Android dictation works with regional preferences set (#…
EhsanulHaqueSiam Nov 6, 2026
ccfbe35
fix(mobile): iOS compose button no longer grows while a tab is presse…
Jerematix Nov 6, 2026
0d8958b
fix(mobile): Android terminal Backspace deletes on the first press (#…
only21mil Nov 6, 2026
35bda28
fix(mobile): Android secret requests no longer offer password manager…
voida6 Nov 6, 2026
0bc7534
fix(mobile): iOS composer text no longer turns invisible mid-draft (#…
wojas Nov 6, 2026
bb61de2
fix(mobile): iOS 26 scroll edge effect no longer hides composer text …
wojas Nov 6, 2026
bd30817
fix(mobile): typing after in-app dictation reaches the draft again on…
wojas Nov 6, 2026
bc9080f
perf(mobile): skip obsolete queued iOS diff row decodes (#25287)
saphid Nov 6, 2026
fab95f3
fix(gitlab): the checks and review list filters work (#30433)
TonybynMp4 Nov 6, 2026
7406ca2
fix(mobile): keep Android tablet cold launches in landscape (#28825)
Dwite Nov 6, 2026
d929e72
fix(pr): GitCafe attachments render in pull request bodies (#31741)
juliusmarminge Nov 6, 2026
bd7da7f
fix(gitlab): unresolved overview threads count as review findings (#3…
TonybynMp4 Nov 6, 2026
1a0f605
fix(mobile): Android alerts and the live card show the same status ba…
juliusmarminge Nov 6, 2026
d6f7554
fix(mobile): keyboard state follows Android's nonanimated IME show (#…
juliusmarminge Nov 6, 2026
61c7601
fix(mobile): Android composer no longer accepts autofill (#27489)
juliusmarminge Nov 6, 2026
391b56a
perf(mobile): skip obsolete queued full token decodes (#25320)
saphid Nov 6, 2026
6593f78
fix(gitlab): threads on changed code show as outdated (#30423)
TonybynMp4 Nov 6, 2026
bda0e7f
fix(gitlab): the checks panel lists each pipeline job (#28728)
TonybynMp4 Nov 6, 2026
8b2c4d1
perf(mobile): avoid code row scans while dragging file paths (#25484)
saphid Nov 6, 2026
c7537d0
fix(chat): show one usage-limit recovery notice (#29292)
shivamhwp Nov 6, 2026
03dbb2f
fix(web): adding to the composer no longer drops earlier findings and…
TonybynMp4 Nov 6, 2026
7bc2a40
feat(pull-requests): PR composers complete @user mentions (#30747)
TonybynMp4 Nov 6, 2026
f08d8dd
feat(web): add pull request comments to the chat (#30579)
TonybynMp4 Nov 6, 2026
42a0e91
feat(gitlab): link issue, merge request, commit and user references (…
TonybynMp4 Nov 6, 2026
7ef841d
fix(mobile): sending on iOS no longer leaves an autocorrected draft b…
saphid Nov 6, 2026
f829c05
feat(gitlab): reviews can request changes (#30549)
TonybynMp4 Nov 6, 2026
e4ff5da
feat(pull-requests): PR composers complete #123 and GitLab !123 refer…
TonybynMp4 Nov 6, 2026
508f869
feat(gitlab): merge requests from forks can toggle maintainer edits (…
TonybynMp4 Nov 6, 2026
c52df24
fix(prs): reviewer menu searches past the first page and keeps GitLab…
TonybynMp4 Nov 6, 2026
bfb3884
fix(ui): snooze menu times match the menu's typography (#30596)
ishaanko Nov 6, 2026
3d78e91
feat(web): a Labs setting hides tool calls so threads read like a cha…
flamboh Nov 6, 2026
4a08786
feat(scheduled-tasks): tasks link to their thread (#31764)
t3dotgg Nov 6, 2026
260d843
fix(shared): recognize numeric bare-repository config (#27500)
macodev00 Nov 6, 2026
a530cab
fix(server): a command id reused for two threads at once no longer su…
saphid Nov 6, 2026
693ac5f
fix(server): coalesce tool output before it fills the live buffer (#1…
snarkyalyx Nov 6, 2026
cedaa0d
perf(server): Windows git reads no longer start a console host (#19288)
AkiVonAkira Nov 6, 2026
37f9134
fix(server): a publish whose first push fails still shows the new rem…
saphid Nov 6, 2026
68d5f7a
fix(server): reverting files removes what a new .gitignore from the t…
saphid Nov 6, 2026
fba1997
fix(server): Codex titles no longer fail on ChatGPT accounts without …
Muzammil-Bit Nov 6, 2026
69726aa
fix(desktop): quit no longer orphans a backend that is slow to stop (…
saphid Nov 6, 2026
596752c
perf(server): GitHub activity reads skip PR-wide fields on later revi…
Yuruzuu Nov 6, 2026
93dbf53
perf(server): the headless browser closes after five idle minutes (51…
Yuruzuu Nov 6, 2026
b15f772
fix(provider-acp-registry): surface agent error message on sign-in fa…
iScabrous Nov 6, 2026
56a8542
perf(server): a slow host no longer delays other watched PRs' wakes (…
Yuruzuu Nov 6, 2026
8dbf262
perf(server): PR cache invalidation no longer waits on other projects…
Yuruzuu Nov 6, 2026
43dfdcc
fix(opencode): tools stopped mid-run show as interrupted, not failed …
saphid Nov 6, 2026
4821696
fix(client-runtime): pull request bodies with CRLF line endings keep …
MarcoDotIO Nov 6, 2026
66a2f35
fix(shared): long assistant quote chips no longer end in a broken cha…
spoukyii Nov 6, 2026
0bf1c9d
fix(cli): t3 connect unlink no longer warns about a server that is go…
MarcoDotIO Nov 6, 2026
396e646
fix(server): ACP agents get T3CODE_THREAD_ID like Claude and Codex se…
frankhommers Nov 6, 2026
c12fd6c
fix(server): accept a case-insensitive DPoP authorization scheme (#30…
spoukyii Nov 6, 2026
144b78a
fix(server): usage-limit auto-resume no longer arms subagents of arch…
spoukyii Nov 6, 2026
9469095
fix(provider-core): background work notices no longer end in a broken…
spoukyii Nov 6, 2026
6f51361
fix(client-runtime): subagent previews no longer end in a broken char…
spoukyii Nov 6, 2026
b1f1731
fix(server): explain fake-IP DNS failures in HTML previews (#30318)
shivamhwp Nov 6, 2026
f63e23e
perf(resource-monitor): Linux terminal process checks use a third of …
jaikhuranna Nov 6, 2026
56d893e
fix(server): failed Claude text generation shows Claude's error inste…
MarcoDotIO Nov 6, 2026
7bdcb1e
fix(server): pull request tools point a bare thread uuid at its mcp: …
spoukyii Nov 6, 2026
b800f99
fix(server): message search keeps results when a short non-Latin word…
MarcoDotIO Nov 6, 2026
e5193ac
fix(web): preview annotation chip labels no longer end in a broken ch…
spoukyii Nov 6, 2026
6b84d2c
fix(server): thread titles no longer fail when the text generation pr…
medric-z Nov 6, 2026
53f1d09
fix(web): refresh resource timeline while diagnostics is open (#30528)
shawk08033 Nov 6, 2026
24fbb5d
fix(usage): a CLIProxy hub saved as its management.html panel URL sho…
Jolymmiles Nov 6, 2026
e4ff843
fix(shared): command output keeps text after an unterminated OSC (#30…
spoukyii Nov 6, 2026
40d7d48
fix(cli): t3 up's retry and restart hints keep --base-dir (#30480)
spoukyii Nov 6, 2026
9bf7dcd
fix(server): a branch tracking another local branch no longer shows t…
MarcoDotIO Nov 6, 2026
1b14cb3
fix(web): terminal links keep route and pnpm paths in one piece (#30603)
tiliakoos Nov 6, 2026
09647d1
fix(shared): CSV and TSV attachments keep their table under generic f…
tiliakoos Nov 6, 2026
20841cd
fix(server): report Claude settings compaction threshold (#29220)
voltcrash Nov 6, 2026
3109638
fix(cli): t3 project remove finds a deleted project by a relative or …
MarcoDotIO Nov 6, 2026
d1667e8
fix(server): an invalid regex in project search still finds its text …
MarcoDotIO Nov 6, 2026
7057a74
fix(server): agents see why a project update was refused instead of a…
MarcoDotIO Nov 6, 2026
66e96e5
perf(client-runtime): stop tracing streamed thread batches (#30679)
ElliotDrel Nov 6, 2026
d0da47a
fix(server): rewound Codex threads import their history from before t…
MarcoDotIO Nov 6, 2026
9ef1256
fix(server): keep writes moving with a larger WAL limit (#31804)
Yash-Singh1 Nov 6, 2026
28aed0d
fix(opencode): edits that delete text keep their empty replacement (#…
Jo2234 Nov 6, 2026
8b9a954
fix(forgejo): read author assignees once (#30686)
Jo2234 Nov 6, 2026
6f96d0e
fix(cli): t3 pair's no-running-server hint keeps --base-dir (#30689)
saphid Nov 6, 2026
9304d84
perf(server): stop tracing routine port polling helpers (#30678)
ElliotDrel Nov 6, 2026
eee9ba1
fix(server): ACP MCP bridge no longer splits messages on U+2028/U+202…
geezoria Nov 6, 2026
6d5772b
fix(cli): expand home-relative thread project paths (#30721)
Jo2234 Nov 6, 2026
5eff606
perf(ci): boot the built web client in Build and Typecheck, not Test …
Pyseph Nov 6, 2026
2b0f08b
fix(shared): ignore unfinished quoted Git config continuations (#30743)
imharjot Nov 6, 2026
cae7e5c
fix(review-guide): preserve excerpt line terminators (#30717)
Jo2234 Nov 6, 2026
0d266fb
feat(pi): show extension status in existing controls (#23901)
StiensWout Nov 6, 2026
e01ca01
fix(fx): preserve literal unicode escape text in clipped replies (#30…
Jo2234 Nov 6, 2026
b60a89f
perf(dev): knip:check runs its two passes side by side (34 s → 26 s m…
Pyseph Nov 6, 2026
cd890ba
fix(shared): command output no longer prints the final byte of escape…
geezoria Nov 6, 2026
10f88f3
fix(web): restore folder breadcrumb trails when reopening tabs (#30706)
Jo2234 Nov 6, 2026
00d1a82
fix(client-runtime): sanitized error stacks keep line and column for …
geezoria Nov 6, 2026
47a89f2
fix(shared): shortened agent paths like /tmp/... no longer become bro…
saphid Nov 6, 2026
1a45858
fix(server): MCP auth spans no longer include the tool call (#30834)
navidnadali Nov 6, 2026
673d9b4
fix(server): preview browser picks up Chrome 157 security fixes (#30820)
snnbotchway Nov 6, 2026
3ca53a9
fix(server): agents read RTF paragraphs that end with a backslash (#3…
geezoria Nov 6, 2026
66fa3fe
fix(server): a late checkpoint keeps the stopped run's baselines (#30…
navidnadali Nov 6, 2026
1058152
fix(shared): preserve IPv6 hosts when switching clone protocols (#30798)
Lucenx9 Nov 6, 2026
ca308a6
docs: explain Linux desktop installation (#30819)
voltcrash Nov 6, 2026
de99330
perf(server): each live event is projected once for all subscribers (…
navidnadali Nov 6, 2026
9a5c197
fix(server): request_secret asks for one value per card (#30805)
rgrvlsk Nov 6, 2026
948f6b5
fix(ssh): host discovery skips hidden Include matches (#30910)
Lucenx9 Nov 6, 2026
5c7e437
docs: clarify diagnostic exports and their separate controls (#30942)
Mr-Neutr0n Nov 6, 2026
444fdb0
docs: fix worktree cleanup path in FAQ (#31000)
jacobcorbett Nov 6, 2026
62dd1a3
fix(server): a policy that manages observability endpoints is refused…
MarcoDotIO Nov 6, 2026
fe199e3
fix(client-runtime): untitled subagent titles no longer end in a brok…
spoukyii Nov 6, 2026
a7f6c68
fix(shared): underline styles and colours no longer scramble command …
MarcoDotIO Nov 6, 2026
bb29ca1
perf(server): batch queued provider writes without waiting (#31656)
Yash-Singh1 Nov 6, 2026
620a37b
fix(server): prepare restart continuations faster on shutdown (#31807)
Yash-Singh1 Nov 6, 2026
0aae722
fix(web): keep change totals aligned beside the label (#31814)
Yash-Singh1 Nov 6, 2026
e9233b0
fix(web): rank usage provider summaries by the selected metric (#15632)
dharmeshgurnani Nov 6, 2026
6f54322
fix(server): wait for automatic follow-ups before settling threads (#…
Yash-Singh1 Nov 6, 2026
2e39f1d
fix(web): settings descriptions use the full row and stay two lines (…
flamboh Nov 6, 2026
bc1eb5d
fix(voice): insert Unicode word boundaries for dictation (#31799)
juliusmarminge Nov 6, 2026
2785e55
fix(mobile): use preferred languages for on-device iPhone dictation (…
juliusmarminge Nov 6, 2026
98b930f
fix(mobile): keep the opening words of local iPhone dictation (#31801)
juliusmarminge Nov 6, 2026
5c004fd
fix(server): keep PR generation focused on the change (#31819)
Yash-Singh1 Nov 6, 2026
e21347b
chore(desktop): trace shutdown phases (#31850)
Yash-Singh1 Nov 6, 2026
1bdb6cf
perf(server): batch shutdown recovery commits (#31854)
Yash-Singh1 Nov 6, 2026
c222511
perf(server): batch projection recovery reads into one query (#31842)
Yash-Singh1 Nov 6, 2026
ccde655
fix(desktop): let the window shrink to 400x360 so it fits half-screen…
shivamhwp Nov 6, 2026
0a2f590
fix(web): improve Changes row text and spacing (#31858)
shivamhwp Nov 6, 2026
dd059b4
feat(server): stop opted-in project actions when threads settle (#31857)
Yash-Singh1 Nov 6, 2026
291bcc3
fix(build): skip Koffi source builds during cross-packaging (#31864)
Yash-Singh1 Nov 6, 2026
c55db3e
fix(server): Antigravity installs on Intel Macs (#30189)
aledog007 Nov 6, 2026
2756da4
fix(desktop): arm64 macOS builds no longer ship x64-only objc-js bina…
joshuarossi Nov 6, 2026
ef6777c
Merge remote-tracking branch 'upstream/main' into yordis/chore-sync-u…
yordis Nov 6, 2026
2e65a44
test(server): align upstream tests with fork launch telemetry and PR …
yordis Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
41 changes: 35 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,41 @@ jobs:
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node apps/desktop/scripts/verify-preload-bundle.mjs

# Browser tests run source modules; this boots the production chunk graph. The
# desktop build above already produced apps/web/dist, and the client waits about
# 15s for a session that never comes, so this runs here instead of in Test Web,
# which gates the whole run. Chromium comes from the cache Test Web shares.
- name: Resolve Playwright version
id: playwright_version
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
run: echo "version=$(node -p "require('./apps/web/node_modules/playwright/package.json').version")" >> "$GITHUB_OUTPUT"

- name: Cache Chromium
id: chromium_cache
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}

- name: Install Chromium for the built web client boot check
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' && steps.chromium_cache.outputs.cache-hit != 'true' }}
run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium

- name: Install missing Chromium system dependencies
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
run: |
browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
test -n "$browser"
if ldd "$browser" | grep '=> not found'; then
node apps/web/node_modules/playwright/cli.js install-deps chromium
fi

- name: Boot the built web client
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.changes.outputs.test_web != 'false' }}
timeout-minutes: 3
run: node apps/web/scripts/verify-built-boot.ts

- name: Typecheck
if: ${{ !cancelled() && steps.typecheck.outcome == 'success' }}
timeout-minutes: 5
Expand Down Expand Up @@ -340,12 +375,6 @@ jobs:
- name: Test
run: vp run --filter @t3tools/web test

# Browser tests run source modules; this boots the production chunk graph.
- name: Boot the built web client
run: |
vp run --filter @t3tools/web build
node apps/web/scripts/verify-built-boot.ts

# apps/server sets `fileParallelism: false`, so its test files run strictly
# one at a time. Sharding spreads them over separate runners instead of
# separate workers, so no two server test files ever share a machine and the
Expand Down
4 changes: 4 additions & 0 deletions .macroscope/ignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,7 @@ native/libghostty-vt/include/**
# it needs. Regenerated by apps/mobile/scripts/generate-highlights.mts; review
# that script and cpp/t3_highlights.c instead.
apps/mobile/modules/t3-highlights/Vendor/**

# Nitro bindings for T3Highlights. Regenerated by `vp run generate:nitro` in
# apps/mobile from modules/t3-highlights/src/T3Highlights.nitro.ts.
apps/mobile/modules/t3-highlights/nitrogen/generated/**
48 changes: 41 additions & 7 deletions apps/desktop/src/app/DesktopApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, it } from "@effect/vitest";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
Expand Down Expand Up @@ -69,14 +70,19 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as MacPermissions from "../permissions/MacPermissions.ts";
import * as PreviewManager from "../preview/Manager.ts";

it.effect(
"starts the backend and window while Linux registration is stalled and cancels it on shutdown",
() =>
it.effect.each([false, true])(
"stops backends together before flushing history and cancels Linux registration, stop defect=%s",
(stopDefect) =>
Effect.gen(function* () {
const registrationStarted = yield* Deferred.make<void>();
const releaseRegistration = yield* Deferred.make<void>();
const registrationStopped = yield* Deferred.make<void>();
const windowStarted = yield* Deferred.make<void>();
const firstStopStarted = yield* Deferred.make<void>();
const secondStopStarted = yield* Deferred.make<void>();
const releaseStops = yield* Deferred.make<void>();
const historyStarted = yield* Deferred.make<void>();
const releaseHistory = yield* Deferred.make<void>();
const quitting = yield* Ref.make(false);
const events: string[] = [];
const record = (event: string) =>
Expand Down Expand Up @@ -169,7 +175,21 @@ it.effect(
start: record("backend"),
currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }),
}),
list: Effect.succeed([]),
list: Effect.succeed([
{
stop: () =>
Deferred.succeed(firstStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
Effect.andThen(stopDefect ? Effect.die("stop defect") : Effect.void),
),
},
{
stop: () =>
Deferred.succeed(secondStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
),
},
]),
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]),
Effect.provideService(DesktopServerExposure.DesktopServerExposure, {
configureFromSettings: () => Effect.succeed({ mode: "local-only" }),
Expand All @@ -182,7 +202,10 @@ it.effect(
start: Effect.void,
} as unknown as DesktopAppActivation.DesktopAppActivation["Service"]),
Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, {
shutdown: Effect.void,
shutdown: Deferred.succeed(historyStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseHistory)),
Effect.asVoid,
),
} as DesktopRendererHistory.DesktopRendererHistory["Service"]),
);
const app = yield* runApp.pipe(
Expand Down Expand Up @@ -226,8 +249,19 @@ it.effect(
yield* Deferred.await(windowStarted);
assert.deepEqual(events, ["identity", "ready", "identity", "backend", "window"]);
assert.isFalse(yield* Deferred.isDone(registrationStopped));
yield* (yield* DesktopShutdown.DesktopShutdown).request;
yield* Fiber.join(app);
const shutdown = yield* DesktopShutdown.DesktopShutdown;
yield* shutdown.request;
yield* Deferred.await(firstStopStarted);
yield* Deferred.await(secondStopStarted);
assert.isFalse(yield* Deferred.isDone(historyStarted));
assert.isFalse(yield* shutdown.isComplete);
yield* Deferred.succeed(releaseStops, undefined);
yield* Deferred.await(historyStarted);
assert.isFalse(yield* shutdown.isComplete);
yield* Deferred.succeed(releaseHistory, undefined);
const result = yield* Fiber.await(app);
assert.equal(Exit.isFailure(result), stopDefect);
assert.isTrue(yield* shutdown.isComplete);
assert.isTrue(yield* Deferred.isDone(registrationStopped));
}).pipe(Effect.provide(DesktopShutdown.layer)),
);
5 changes: 3 additions & 2 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Cause from "effect/Cause";
import * as Config from "effect/Config";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
Expand All @@ -19,6 +18,7 @@ import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as PreviewPasskeys from "../preview/Passkeys.ts";
import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
Expand Down Expand Up @@ -160,7 +160,7 @@ const stopAllPoolInstances = Effect.fn("desktop.app.stopAllPoolInstances")(
const instances = yield* pool.list;
yield* Effect.forEach(
instances,
(instance) => instance.stop({ timeout: Duration.seconds(5) }),
(instance) => instance.stop({ timeout: DesktopBackendManager.BACKEND_QUIT_STOP_TIMEOUT }),
{ concurrency: "unbounded" },
);
},
Expand Down Expand Up @@ -363,6 +363,7 @@ const scopedProgram = Effect.scoped(
stopAllPoolInstances().pipe(
Effect.ensuring(rendererHistory.shutdown),
Effect.ensuring(shutdown.markComplete),
Effect.withSpan("desktop.app.shutdown"),
),
);

Expand Down
54 changes: 54 additions & 0 deletions apps/desktop/src/backend/DesktopBackendManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,4 +720,58 @@ describe("DesktopBackendManager", () => {
}).pipe(Effect.provide(TestClock.layer())),
),
);

it.effect("the quit stop deadline waits for a backend that only exits at the force kill", () =>
Effect.scoped(
Effect.gen(function* () {
const spawned = yield* Deferred.make<void>();
const teardownStarted = yield* Deferred.make<void>();
const teardownFinished = yield* Deferred.make<void>();
// The real spawner sends SIGTERM, waits the grace, then SIGKILLs. A backend that
// ignores SIGTERM therefore finishes tearing down just after the grace.
const instance = yield* makeTestInstance({
spawnerLayer: Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() =>
Effect.gen(function* () {
const scope = yield* Scope.Scope;
yield* Deferred.succeed(spawned, undefined);
yield* Scope.addFinalizer(
scope,
Deferred.succeed(teardownStarted, undefined).pipe(
Effect.andThen(Effect.sleep(Duration.seconds(6))),
Effect.andThen(Deferred.succeed(teardownFinished, undefined)),
Effect.asVoid,
),
);
return makeProcess({
exitCode: Deferred.await(teardownFinished).pipe(
Effect.as(ChildProcessSpawner.ExitCode(0)),
),
});
}),
),
),
httpClientLayer: layerHttpClient(() => Effect.never),
});
yield* instance.start;
yield* Deferred.await(spawned);

// App quit and the updater stop every backend with this deadline.
const quitDone = yield* Deferred.make<void>();
const quitFiber = yield* instance
.stop({ timeout: DesktopBackendManager.BACKEND_QUIT_STOP_TIMEOUT })
.pipe(Effect.andThen(Deferred.succeed(quitDone, undefined)), Effect.forkChild);

yield* Deferred.await(teardownStarted);
// Past the SIGTERM grace but before the force kill lands: quit must still be waiting.
yield* TestClock.adjust(Duration.millis(5_500));
assert.isFalse(yield* Deferred.isDone(quitDone));

yield* TestClock.adjust(Duration.millis(500));
yield* Fiber.join(quitFiber);
assert.isTrue(yield* Deferred.isDone(teardownFinished));
}).pipe(Effect.provide(TestClock.layer())),
),
);
});
13 changes: 12 additions & 1 deletion apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ const DEFAULT_BACKEND_READINESS_INTERVAL = Duration.millis(100);
const DEFAULT_BACKEND_READINESS_REQUEST_TIMEOUT = Duration.seconds(1);
// Match the service launcher's grace period so backends can stop provider sessions.
const DEFAULT_BACKEND_TERMINATE_GRACE = Duration.seconds(5);
// Allow SIGKILL and the spawner exit wait after the SIGTERM grace so quit cannot
// orphan a backend whose shutdown hangs.
export const BACKEND_QUIT_STOP_TIMEOUT = Duration.sum(
DEFAULT_BACKEND_TERMINATE_GRACE,
Duration.seconds(2),
);
const DEFAULT_BACKEND_OUTPUT_DRAIN_TIMEOUT = Duration.seconds(5);
const BACKEND_READINESS_PATH = "/.well-known/t3/environment";
// Three restarts without readiness is about 2 s of backoff: past a blip, early enough to help.
Expand Down Expand Up @@ -306,7 +312,10 @@ const closeRun = (
onNone: () => Effect.void,
onSome: (fiber) => Fiber.await(fiber).pipe(Effect.asVoid),
});
const close = Scope.close(run.scope, Exit.void).pipe(Effect.andThen(waitForFiber));
const close = Scope.close(run.scope, Exit.void).pipe(
Effect.withSpan("desktop.backendInstance.closeScope"),
Effect.andThen(waitForFiber.pipe(Effect.withSpan("desktop.backendInstance.awaitRun"))),
);
const timeout = options?.timeout;

if (!timeout) {
Expand Down Expand Up @@ -567,6 +576,7 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* (
yield* probeReadiness().pipe(Effect.repeat({ while: (ready) => !ready }), Effect.forkScoped);

const exit = yield* handle.exitCode.pipe(
Effect.withSpan("desktop.backendProcess.awaitExit"),
Effect.map((exitCode) => ({
code: Option.some(exitCode),
reason: `code=${exitCode}`,
Expand Down Expand Up @@ -602,6 +612,7 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* (
}).pipe(
Effect.timeout(options.outputDrainTimeout ?? DEFAULT_BACKEND_OUTPUT_DRAIN_TIMEOUT),
Effect.ignore,
Effect.withSpan("desktop.backendProcess.drainOutput"),
);
if (Exit.isFailure(exit)) {
return yield* Effect.failCause(exit.cause);
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export const CHECK_SYSTEM_PERMISSION_CHANNEL = "desktop:check-system-permission"

export const PREVIEW_RECORDING_INPUT_CHANNEL = "desktop:preview-recording-input";

export const PREVIEW_ANNOTATION_VOICE_EVENT_CHANNEL = "desktop:preview-annotation-voice-event";
export const PREVIEW_ANNOTATION_VOICE_STATE_CHANNEL = "desktop:preview-annotation-voice-state";

export const RECEIVE_PROVIDER_AUTH_CALLBACK_CHANNEL = "desktop:receive-provider-auth-callback";
export const CANCEL_PROVIDER_AUTH_CALLBACK_CHANNEL = "desktop:cancel-provider-auth-callback";
export const TAKE_LEGACY_LOCAL_STORAGE_CHANNEL = "desktop:take-legacy-local-storage";
Expand Down
19 changes: 16 additions & 3 deletions apps/desktop/src/ipc/methods/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
DesktopPreviewAnnotationSendEnabledInputSchema,
DesktopPreviewAnnotationThemeInputSchema,
DesktopPreviewAnnotationVoiceStateSchema,
DesktopPreviewPickElementInputSchema,
DesktopPreviewArtifactInputSchema,
DesktopPreviewConfigInputSchema,
DesktopPreviewNavigateInputSchema,
Expand Down Expand Up @@ -363,11 +365,21 @@ const setAnnotationTheme = DesktopIpc.makeIpcMethod({

const pickElement = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL,
payload: DesktopPreviewTabInputSchema,
payload: DesktopPreviewPickElementInputSchema,
result: Schema.NullOr(PreviewAnnotationSubmissionResultSchema),
handler: Effect.fn("desktop.ipc.preview.pickElement")(function* ({ tabId }) {
handler: Effect.fn("desktop.ipc.preview.pickElement")(function* ({ tabId, voice }) {
const manager = yield* PreviewManager.PreviewManager;
return yield* manager.pickElement(tabId, voice);
}),
});

const updateAnnotationVoice = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_ANNOTATION_VOICE_STATE_CHANNEL,
payload: DesktopPreviewAnnotationVoiceStateSchema,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.preview.updateAnnotationVoice")(function* (state) {
const manager = yield* PreviewManager.PreviewManager;
return yield* manager.pickElement(tabId);
yield* manager.updateAnnotationVoice(state);
}),
});

Expand Down Expand Up @@ -428,6 +440,7 @@ const saveRecording = DesktopIpc.makeIpcMethod({

export const methods = [
setForwardedShortcuts,
updateAnnotationVoice,
createTab,
closeTab,
registerWebview,
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
DesktopBridge,
DesktopPreviewAnnotationVoiceEvent,
DesktopPreviewOpenLinkEvent,
DesktopPreviewPointerEvent,
DesktopPreviewRecordingInputEvent,
Expand Down Expand Up @@ -368,7 +369,20 @@ contextBridge.exposeInMainWorld("desktopBridge", {
tabId,
enabled,
}),
pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
annotationVoice: {
onEvent: (listener) => {
const onEvent = (_event: Electron.IpcRendererEvent, tabId: string, value: unknown) => {
if (typeof tabId !== "string" || typeof value !== "object" || value === null) return;
listener(tabId, value as DesktopPreviewAnnotationVoiceEvent);
};
ipcRenderer.on(IpcChannels.PREVIEW_ANNOTATION_VOICE_EVENT_CHANNEL, onEvent);
return () => ipcRenderer.off(IpcChannels.PREVIEW_ANNOTATION_VOICE_EVENT_CHANNEL, onEvent);
},
update: (state) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_ANNOTATION_VOICE_STATE_CHANNEL, state),
},
pickElement: (tabId, voice) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId, voice }),
cancelPickElement: (tabId) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),
captureScreenshot: (tabId) =>
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/preview/Annotation.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
--t3-popover-foreground: oklch(0.269 0 0);
--t3-primary: oklch(0.488 0.217 264);
--t3-primary-foreground: white;
--t3-destructive: oklch(0.577 0.245 27.325);
--t3-muted: rgb(0 0 0 / 4%);
--t3-muted-foreground: oklch(0.556 0 0);
--t3-accent: rgb(0 0 0 / 4%);
Expand Down Expand Up @@ -66,3 +67,8 @@ textarea:focus-visible {
outline: 2px solid color-mix(in srgb, var(--t3-ring) 72%, transparent);
outline-offset: -2px;
}

/* Utility display classes must not override the native hidden state. */
[hidden] {
display: none !important;
}
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/AnnotationStyles.generated.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions apps/desktop/src/preview/GuestProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const RECORDING_CONTROLLER_CHANNEL = "preview:recording-controller";
export const PASSKEY_CREATE_CHANNEL = "preview:passkey-create";
export const PASSKEY_GET_CHANNEL = "preview:passkey-get";
export const PASSKEY_BRIDGE_ARGUMENT = "--t3code-preview-passkey-bridge";
export const ANNOTATION_VOICE_EVENT_CHANNEL = "preview:annotation-voice-event";
export const ANNOTATION_VOICE_STATE_CHANNEL = "preview:annotation-voice-state";
Loading
Loading