From 2fdc704bbb2c6b9e0ba01edbc21f3aa2c956d698 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 15 Jul 2026 00:08:33 -0700 Subject: [PATCH 01/19] perf(web): duty-cycle status animations and remove fixed noise overlay (#3978) --- .../src/components/ConnectionStatusDot.tsx | 2 +- apps/web/src/components/Sidebar.tsx | 6 +- .../src/components/ThreadStatusIndicators.tsx | 8 +- .../src/components/chat/MessagesTimeline.tsx | 6 +- .../components/preview/AgentBrowserCursor.tsx | 2 +- .../components/preview/PreviewChromeRow.tsx | 2 +- .../preview/PreviewLocalServerCard.tsx | 2 +- apps/web/src/components/ui/sidebar.tsx | 8 +- apps/web/src/index.css | 89 ++++++++++++++++--- 9 files changed, 96 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/ConnectionStatusDot.tsx b/apps/web/src/components/ConnectionStatusDot.tsx index b12acf1ba40d..0c22f1702e5e 100644 --- a/apps/web/src/components/ConnectionStatusDot.tsx +++ b/apps/web/src/components/ConnectionStatusDot.tsx @@ -17,7 +17,7 @@ export function ConnectionStatusDot({ {pingClassName ? ( diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 21525b56b770..bb6b2752a17a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -771,7 +771,9 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr /> } > - + {terminalStatus.label} @@ -2226,7 +2228,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index 3e85920d1904..55f9fbfdc044 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -149,7 +149,7 @@ export function ThreadStatusLabel({ > @@ -170,7 +170,7 @@ export function ThreadStatusLabel({ > {status.label} @@ -276,7 +276,9 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma /> } > - + {terminalStatus.label} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 1a4dc6b68955..61d7855844d1 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1055,9 +1055,9 @@ function WorkingTimelineRow({ row }: { row: Extract
- - - + + + {row.createdAt ? ( diff --git a/apps/web/src/components/preview/AgentBrowserCursor.tsx b/apps/web/src/components/preview/AgentBrowserCursor.tsx index ca6c2ff72359..bc89daee4595 100644 --- a/apps/web/src/components/preview/AgentBrowserCursor.tsx +++ b/apps/web/src/components/preview/AgentBrowserCursor.tsx @@ -66,7 +66,7 @@ function AgentBrowserCursorEvent(props: { {event.phase === "click" ? ( ) : null} {recording ? ( - + ) : null} diff --git a/apps/web/src/components/preview/PreviewLocalServerCard.tsx b/apps/web/src/components/preview/PreviewLocalServerCard.tsx index 54a020cbf655..c7b08ad2893d 100644 --- a/apps/web/src/components/preview/PreviewLocalServerCard.tsx +++ b/apps/web/src/components/preview/PreviewLocalServerCard.tsx @@ -36,7 +36,7 @@ function describeServer(server: PreviewableServer): string { function PulsingDot() { return ( - + ); diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 097568f77f0e..50fb652a4955 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -216,7 +216,7 @@ function Sidebar({
@@ -622,7 +622,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { return (
Date: Wed, 15 Jul 2026 14:23:48 +0530 Subject: [PATCH 02/19] fix(docs): correct CI task-runner commands in ci.md (#3990) --- docs/operations/ci.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/ci.md b/docs/operations/ci.md index d030b446b6a4..7a0447ec0700 100644 --- a/docs/operations/ci.md +++ b/docs/operations/ci.md @@ -1,6 +1,6 @@ # CI quality gates -- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`. +- `.github/workflows/ci.yml` runs `vp check` (lint + typecheck), `vpr typecheck`, and `vp run test` on pull requests and pushes to `main`. - `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. - The release workflow auto-enables signing only when platform credentials are present. macOS passkey builds additionally require `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. Without the core signing credentials, it still releases unsigned artifacts. - See [Release Checklist](./release.md) for the full release/signing setup checklist. From c69120f4845a13d0b1cc155995859957935f9007 Mon Sep 17 00:00:00 2001 From: Kriday Dave Date: Wed, 15 Jul 2026 14:24:40 +0530 Subject: [PATCH 03/19] fix(docs): repair broken source links in architecture overview (#3991) --- docs/architecture/overview.md | 44 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index ce5a0afe92a1..a7b777fb7732 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -60,11 +60,11 @@ sequenceDiagram Transport-->>Browser: Hydrate initial state ``` -1. The browser boots [`WsTransport`][1] and registers typed listeners in [`wsNativeApi`][2]. -2. The server accepts the connection in [`wsServer`][3] and brings up the runtime graph defined in [`serverLayers`][7]. -3. [`ServerReadiness`][4] waits until the key startup barriers are complete. -4. Once the server is ready, [`wsServer`][3] sends `server.welcome` from the contracts in [`ws.ts`][6] through [`ServerPushBus`][5]. -5. The browser receives that ordered push through [`WsTransport`][1], and [`wsNativeApi`][2] uses it to seed local client state. +1. The browser boots `WsTransport` and registers typed listeners in `wsNativeApi`. +2. The server accepts the connection in `wsServer` and brings up the runtime graph defined in `serverLayers`. +3. `ServerReadiness` waits until the key startup barriers are complete. +4. Once the server is ready, `wsServer` sends `server.welcome` from the contracts in `ws.ts` through `ServerPushBus`. +5. The browser receives that ordered push through `WsTransport`, and `wsNativeApi` uses it to seed local client state. ### User turn flow @@ -90,12 +90,12 @@ sequenceDiagram Push-->>Browser: Typed push ``` -1. A user action in the browser becomes a typed request through [`WsTransport`][1] and the browser API layer in [`nativeApi`][12]. -2. [`wsServer`][3] decodes that request using the shared WebSocket contracts in [`ws.ts`][6] and routes it to the right service. +1. A user action in the browser becomes a typed request through `WsTransport` and the browser API layer in `nativeApi`. +2. `wsServer` decodes that request using the shared WebSocket contracts in `ws.ts` and routes it to the right service. 3. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio. 4. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events. 5. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events. -6. [`wsServer`][3] pushes those updates to the browser through [`ServerPushBus`][5] on channels defined in [`orchestration.ts`][11]. +6. `wsServer` pushes those updates to the browser through `ServerPushBus` on channels defined in [`orchestration.ts`][11]. ### Async completion flow @@ -123,21 +123,13 @@ sequenceDiagram 2. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic. 3. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence. 4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers. -5. Any user-visible state changes produced by that async work still go back through [`wsServer`][3] and [`ServerPushBus`][5]. - -[1]: ../apps/web/src/wsTransport.ts -[2]: ../apps/web/src/wsNativeApi.ts -[3]: ../apps/server/src/wsServer.ts -[4]: ../apps/server/src/wsServer/readiness.ts -[5]: ../apps/server/src/wsServer/pushBus.ts -[6]: ../packages/contracts/src/ws.ts -[7]: ../apps/server/src/serverLayers.ts -[8]: ../apps/server/src/provider/Layers/ProviderService.ts -[9]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts -[10]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts -[11]: ../packages/contracts/src/orchestration.ts -[12]: ../apps/web/src/nativeApi.ts -[13]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts -[14]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts -[15]: ../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts -[16]: ../packages/shared/src/DrainableWorker.ts +5. Any user-visible state changes produced by that async work still go back through `wsServer` and `ServerPushBus`. + +[8]: ../../apps/server/src/provider/Layers/ProviderService.ts +[9]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +[10]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts +[11]: ../../packages/contracts/src/orchestration.ts +[13]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +[14]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts +[15]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts +[16]: ../../packages/shared/src/DrainableWorker.ts From 462442667e815abd822b042d53924775460b7bcf Mon Sep 17 00:00:00 2001 From: Kriday Dave Date: Wed, 15 Jul 2026 14:25:01 +0530 Subject: [PATCH 04/19] fix(docs): replace stale codething-mvp absolute paths with repo-relative links (#3992) --- docs/operations/effect-fn-checklist.md | 178 ++++++++++++------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/docs/operations/effect-fn-checklist.md b/docs/operations/effect-fn-checklist.md index 279b5646d32e..938dea8d6815 100644 --- a/docs/operations/effect-fn-checklist.md +++ b/docs/operations/effect-fn-checklist.md @@ -56,43 +56,43 @@ Effect.fn("name")( ### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`) -- [x] [buildUserMessageEffect](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L554) -- [x] [makeClaudeAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L913) -- [x] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414) -- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887) -- [x] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975) -- [x] [readThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984) -- [x] [rollbackThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990) -- [x] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeAdapter.ts#L3039) +- [x] [buildUserMessageEffect](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L554) +- [x] [makeClaudeAdapter](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L913) +- [x] [startSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414) +- [x] [sendTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887) +- [x] [interruptTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975) +- [x] [readThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984) +- [x] [rollbackThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990) +- [x] [stopSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L3039) - [x] Internal helpers and callback wrappers in this file ### `apps/server/src/git/Layers/GitCore.ts` (`58`) -- [x] [makeGitCore](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L513) -- [x] [handleTraceLine](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L324) -- [x] [emitCompleteLines](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L455) -- [x] [commit](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1190) -- [x] [pushCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1223) -- [x] [pullCurrentBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1323) -- [x] [checkoutBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitCore.ts#L1727) +- [x] [makeGitCore](../../apps/server/src/git/Layers/GitCore.ts#L513) +- [x] [handleTraceLine](../../apps/server/src/git/Layers/GitCore.ts#L324) +- [x] [emitCompleteLines](../../apps/server/src/git/Layers/GitCore.ts#L455) +- [x] [commit](../../apps/server/src/git/Layers/GitCore.ts#L1190) +- [x] [pushCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1223) +- [x] [pullCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1323) +- [x] [checkoutBranch](../../apps/server/src/git/Layers/GitCore.ts#L1727) - [x] Service methods and callback wrappers in this file ### `apps/server/src/git/Layers/GitManager.ts` (`28`) -- [x] [configurePullRequestHeadUpstream](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L387) -- [x] [materializePullRequestHeadBranch](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L428) -- [x] [findOpenPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L576) -- [x] [findLatestPr](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L602) -- [x] [runCommitStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L728) -- [x] [runPrStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L842) -- [x] [runFeatureBranchStep](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/GitManager.ts#L1106) +- [x] [configurePullRequestHeadUpstream](../../apps/server/src/git/Layers/GitManager.ts#L387) +- [x] [materializePullRequestHeadBranch](../../apps/server/src/git/Layers/GitManager.ts#L428) +- [x] [findOpenPr](../../apps/server/src/git/Layers/GitManager.ts#L576) +- [x] [findLatestPr](../../apps/server/src/git/Layers/GitManager.ts#L602) +- [x] [runCommitStep](../../apps/server/src/git/Layers/GitManager.ts#L728) +- [x] [runPrStep](../../apps/server/src/git/Layers/GitManager.ts#L842) +- [x] [runFeatureBranchStep](../../apps/server/src/git/Layers/GitManager.ts#L1106) - [x] Remaining helpers and nested callback wrappers in this file ### `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (`25`) -- [x] [runProjectorForEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161) -- [x] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357) -- [x] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415) +- [x] [runProjectorForEvent](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161) +- [x] [applyProjectsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357) +- [x] [applyThreadsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415) - [x] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250` - [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264` - [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305` @@ -100,98 +100,98 @@ Effect.fn("name")( ### `apps/server/src/provider/Layers/ProviderService.ts` (`24`) -- [ ] [makeProviderService](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L134) -- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L196) -- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L255) -- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L284) -- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L347) -- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L393) -- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L411) -- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L430) -- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L445) -- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L466) -- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L516) -- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L538) +- [ ] [makeProviderService](../../apps/server/src/provider/Layers/ProviderService.ts#L134) +- [ ] [recoverSessionForThread](../../apps/server/src/provider/Layers/ProviderService.ts#L196) +- [ ] [resolveRoutableSession](../../apps/server/src/provider/Layers/ProviderService.ts#L255) +- [ ] [startSession](../../apps/server/src/provider/Layers/ProviderService.ts#L284) +- [ ] [sendTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L347) +- [ ] [interruptTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L393) +- [ ] [respondToRequest](../../apps/server/src/provider/Layers/ProviderService.ts#L411) +- [ ] [respondToUserInput](../../apps/server/src/provider/Layers/ProviderService.ts#L430) +- [ ] [stopSession](../../apps/server/src/provider/Layers/ProviderService.ts#L445) +- [ ] [listSessions](../../apps/server/src/provider/Layers/ProviderService.ts#L466) +- [ ] [rollbackConversation](../../apps/server/src/provider/Layers/ProviderService.ts#L516) +- [ ] [runStopAll](../../apps/server/src/provider/Layers/ProviderService.ts#L538) ### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`) -- [x] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680) -- [x] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722) -- [x] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761) -- [x] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800) -- [x] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908) +- [x] [finalizeAssistantMessage](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680) +- [x] [upsertProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722) +- [x] [finalizeBufferedProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761) +- [x] [clearTurnStateForSession](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800) +- [x] [processRuntimeEvent](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908) - [x] Nested callback wrappers in this file ### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`) -- [x] [makeCodexAdapter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1317) -- [x] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1399) -- [x] [writeNativeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1546) -- [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555) +- [x] [makeCodexAdapter](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1317) +- [x] [sendTurn](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1399) +- [x] [writeNativeEvent](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1546) +- [x] [listener](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1555) - [x] Remaining nested callback wrappers in this file ### `apps/server/src/checkpointing/CheckpointStore.ts` (`10`) -- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L123) -- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L137) -- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L144) -- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L151) +- [ ] [captureCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L123) +- [ ] [restoreCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L137) +- [ ] [diffCheckpoints](../../apps/server/src/checkpointing/CheckpointStore.ts#L144) +- [ ] [deleteCheckpointRefs](../../apps/server/src/checkpointing/CheckpointStore.ts#L151) - [ ] Nested callback wrappers in this file ### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`) -- [ ] [toLogMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77) -- [ ] [makeThreadWriter](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102) -- [ ] [makeEventNdjsonLogger](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174) -- [ ] [write](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231) -- [ ] [close](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/EventNdjsonLogger.ts#L247) +- [ ] [toLogMessage](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77) +- [ ] [makeThreadWriter](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102) +- [ ] [makeEventNdjsonLogger](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174) +- [ ] [write](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231) +- [ ] [close](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L247) - [ ] Flush and writer-resolution callback wrappers in this file ### `apps/server/scripts/cli.ts` (`8`) -- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L125) -- [ ] Command handlers around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L170) -- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L221) -- [ ] Resource callbacks around [cli.ts](/Users/julius/Development/Work/codething-mvp/apps/server/scripts/cli.ts#L239) +- [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L125) +- [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L170) +- [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L221) +- [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L239) ### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`) -- [ ] [processEnvelope](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64) -- [ ] [dispatch](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218) -- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162) -- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L200) +- [ ] [processEnvelope](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64) +- [ ] [dispatch](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218) +- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162) +- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L200) ### `apps/server/src/orchestration/projector.ts` (`5`) -- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L242) -- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L336) -- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L397) -- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L446) -- [ ] `switch` branch wrapper at [projector.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/projector.ts#L478) +- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L242) +- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L336) +- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L397) +- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L446) +- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L478) ### Smaller clusters -- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`) -- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`) -- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`) -- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`) -- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`) -- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`) -- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`) -- [ ] [apps/server/src/keybindings.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/keybindings.ts) (`4`) -- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`) -- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`) -- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`) -- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`) -- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`) -- [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`) -- [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`) -- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) -- [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`) -- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`) +- [ ] [packages/shared/src/DrainableWorker.ts](../../packages/shared/src/DrainableWorker.ts) (`4`) +- [ ] [apps/server/src/wsServer/pushBus.ts](../../apps/server/src/wsServer/pushBus.ts) (`4`) +- [ ] [apps/server/src/wsServer.ts](../../apps/server/src/wsServer.ts) (`4`) +- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](../../apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`) +- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](../../apps/server/src/persistence/Layers/Sqlite.ts) (`4`) +- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`) +- [ ] [apps/server/src/main.ts](../../apps/server/src/main.ts) (`4`) +- [ ] [apps/server/src/keybindings.ts](../../apps/server/src/keybindings.ts) (`4`) +- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](../../apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`) +- [ ] [apps/server/src/serverLayers.ts](../../apps/server/src/serverLayers.ts) (`3`) +- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](../../apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`) +- [ ] [apps/server/src/telemetry/Identify.ts](../../apps/server/src/telemetry/Identify.ts) (`2`) +- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](../../apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`) +- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](../../apps/server/src/provider/Layers/CodexProvider.ts) (`2`) +- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](../../apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`) +- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](../../apps/server/src/persistence/NodeSqliteClient.ts) (`2`) +- [ ] [apps/server/src/persistence/Migrations.ts](../../apps/server/src/persistence/Migrations.ts) (`2`) +- [ ] [apps/server/src/open.ts](../../apps/server/src/open.ts) (`2`) +- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](../../apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) +- [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](../../apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`) +- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](../../apps/server/src/provider/makeManagedServerProvider.ts) (`1`) ``` From d359789ed4ed99b19a0bc6db3c6f6eba4d2b53ce Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 15 Jul 2026 11:44:03 +0200 Subject: [PATCH 05/19] docs: Add T3 Code Legal Docs (#3972) Co-authored-by: codex --- apps/marketing/src/components/LegalPage.astro | 414 ++++++++++++++++ apps/marketing/src/layouts/Layout.astro | 33 +- apps/marketing/src/pages/legal.astro | 172 +++++++ apps/marketing/src/pages/privacy-policy.astro | 445 ++++++++++++++++++ .../marketing/src/pages/security-policy.astro | 187 ++++++++ .../src/pages/terms-of-service.astro | 396 ++++++++++++++++ apps/mobile/package.json | 2 +- apps/mobile/src/Stack.tsx | 22 + .../settings/SettingsLegalRouteScreen.tsx | 6 + .../features/settings/SettingsRouteScreen.tsx | 1 + .../SettingsLegalDocumentRouteScreen.tsx | 185 ++++++++ .../settings/components/SettingsRow.tsx | 17 +- .../components/settings-sheet-targets.ts | 2 + .../settings/lib/legal-document-url.test.ts | 24 + .../settings/lib/legal-document-url.ts | 58 +++ 15 files changed, 1960 insertions(+), 4 deletions(-) create mode 100644 apps/marketing/src/components/LegalPage.astro create mode 100644 apps/marketing/src/pages/legal.astro create mode 100644 apps/marketing/src/pages/privacy-policy.astro create mode 100644 apps/marketing/src/pages/security-policy.astro create mode 100644 apps/marketing/src/pages/terms-of-service.astro create mode 100644 apps/mobile/src/features/settings/SettingsLegalRouteScreen.tsx create mode 100644 apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx create mode 100644 apps/mobile/src/features/settings/lib/legal-document-url.test.ts create mode 100644 apps/mobile/src/features/settings/lib/legal-document-url.ts diff --git a/apps/marketing/src/components/LegalPage.astro b/apps/marketing/src/components/LegalPage.astro new file mode 100644 index 000000000000..eb824039e980 --- /dev/null +++ b/apps/marketing/src/components/LegalPage.astro @@ -0,0 +1,414 @@ +--- +import Layout from "../layouts/Layout.astro"; + +interface Props { + readonly title: string; + readonly description: string; + readonly heading: string; + readonly lede: string; + readonly effectiveDate: string; + readonly lastUpdated: string; + readonly sections: ReadonlyArray; +} + +const { title, description, heading, lede, effectiveDate, lastUpdated, sections } = Astro.props; +--- + + + + + + + + diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro index 5d9fc4e8f3bc..9c454c4b78bd 100644 --- a/apps/marketing/src/layouts/Layout.astro +++ b/apps/marketing/src/layouts/Layout.astro @@ -4,11 +4,13 @@ import { GITHUB_REPOSITORY_URL, MARKETING_STATS } from "../lib/site"; interface Props { title?: string; description?: string; + pageClass?: string; } const { title = "T3 Code", description = "T3 Code — The open-source control plane for coding agents.", + pageClass, } = Astro.props; --- @@ -30,7 +32,7 @@ const { {title} -
+ @@ -279,7 +284,7 @@ const { min-height: 100vh; display: flex; flex-direction: column; - overflow-x: hidden; + overflow-x: clip; } .nav { @@ -402,6 +407,8 @@ const { .footer-links { display: flex; + flex-wrap: wrap; + justify-content: flex-end; gap: 20px; } @@ -421,6 +428,28 @@ const { padding-left: 20px; padding-right: 20px; } + + .legal-document-page .nav { + display: none; + } + + .footer { + padding-bottom: max(32px, env(safe-area-inset-bottom)); + } + + .footer-inner { + align-items: flex-start; + flex-direction: column; + gap: 28px; + } + + .footer-links { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + width: 100%; + gap: 14px 20px; + justify-content: initial; + } } @media (max-width: 420px) { diff --git a/apps/marketing/src/pages/legal.astro b/apps/marketing/src/pages/legal.astro new file mode 100644 index 000000000000..d75413a8da7e --- /dev/null +++ b/apps/marketing/src/pages/legal.astro @@ -0,0 +1,172 @@ +--- +import Layout from "../layouts/Layout.astro"; + +const documents = [ + { + number: "01", + title: "Privacy Policy", + description: "How T3 Code handles information, connected services, and your privacy choices.", + href: "/privacy-policy", + }, + { + number: "02", + title: "Terms of Service", + description: "The terms that govern your use of T3 Code and T3 Tools-operated services.", + href: "/terms-of-service", + }, + { + number: "03", + title: "Security Policy", + description: "Our security practices, responsible disclosure process, and shared responsibilities.", + href: "/security-policy", + }, +] as const; +--- + + + + + + diff --git a/apps/marketing/src/pages/privacy-policy.astro b/apps/marketing/src/pages/privacy-policy.astro new file mode 100644 index 000000000000..b85bc0b12b44 --- /dev/null +++ b/apps/marketing/src/pages/privacy-policy.astro @@ -0,0 +1,445 @@ +--- +import LegalPage from "../components/LegalPage.astro"; + +const sections = [ + ["introduction", "Introduction"], + ["information", "Personal information we collect"], + ["uses", "How we use your personal information"], + ["sharing", "How we share your personal information"], + ["choices", "Your choices regarding your personal information"], + ["security", "Security of your personal information"], + ["international", "International users"], + ["children", "Children"], + ["do-not-track", "Do Not Track"], + ["updates", "Updates to this Privacy Policy"], + ["contact", "Contact us"], +] as const; +--- + + +
+

01

+

Introduction

+

+ This Privacy Policy (the “Policy”) describes how T3 Tools, Inc. (the “Company,” “T3 + Tools,” “we,” or “us”) collects, uses, and shares personal information when you use the T3 + Code desktop and mobile applications, the hosted T3 Code web application located at + {" "}https://app.t3.codes, or the optional T3 Connect + service (together, the “Services”), or visit the T3 Code marketing website located at + {" "}https://t3.codes (the “Site”). This Policy applies to + personal information we collect through the Site and the Services, as well as personal + information you provide to us directly. +

+

+ By using the Site or the Services, you acknowledge the practices described in this + Policy. If you do not agree with this Policy, please do not use the Site or the Services. + This Policy does not govern third-party coding agents, artificial-intelligence providers, + identity providers, or environments that you choose to connect to T3 Code; those third + parties process information under their own terms and privacy policies. +

+
+ +
+

02

+

Personal Information We Collect

+ +

+ We collect personal information about you in different ways depending on how you use + T3 Code and which optional features you enable. +

+ +

Personal Information From Users of T3 Code

+ +

+ When you use T3 Code, we may collect the following categories of personal information: +

+
    +
  • + Account information. If you create or sign in to a T3 account, we + receive general identifiers and profile information from our authentication provider + and the identity provider you select. This may include your name, email address, + profile image, user ID, sign-in method, and authentication credentials or tokens. +
  • +
  • + Environment and device information. If you enable T3 Connect, we + process information needed to connect and secure your environments and devices, + including identifiers, labels you assign, public keys, connection endpoints, + credential hashes, connection status, app and operating-system versions, and + notification preferences. +
  • +
  • + Agent activity and notifications. If you enable notifications or Live + Activities, we process push tokens and limited activity information needed to provide + those features, such as project and thread titles, model name, status, and activity + headline. +
  • +
  • + Information you provide to us. We collect information that you choose + to provide when you request support, send feedback, or otherwise communicate with us. +
  • +
+ +

Personal Information From Users of Our Site Generally

+

+ When you visit the Site or use online portions of the Services, we and our service + providers may automatically log online identifiers and internet-activity information, + including your Internet Protocol (IP) address, device and operating-system type, browser + or app type and version, pages viewed, access times, request timestamps, connection + events, latency, error details, and diagnostic traces. The amount of information we + collect depends on the Services you use and your device and browser settings. +

+

+ We use operational and diagnostic information to provide, secure, maintain, and + troubleshoot the Services, not for targeted advertising. +

+ +

Information We Get From Others

+

+ We may receive personal information from service providers that help us operate the + Services, such as authentication and identity providers, app stores, device-platform + providers, and coding-agent providers you choose to use. We may combine this information + with information we collect through the Site and the Services. +

+ +

Cookies and Local Storage

+

+ The Site and online portions of the Services may use cookies or similar technologies to + operate essential features, maintain sessions, remember preferences, and understand + whether the Services are functioning correctly. T3 Code also stores connection settings, + credentials, preferences, drafts, and cached thread, file, branch, and server metadata on + your device. Credentials and session tokens use platform-provided protected storage where + supported. Information stored only on your device is not collected by T3 Tools unless it + is transmitted as part of a feature you use. +

+ +

Device Permissions

+
    +
  • + Camera. Used when you choose to scan a pairing QR code. Camera frames + are processed on your device. +
  • +
  • + Photos and files. Items you select are accessed only to preview them or + attach them to a request you initiate. +
  • +
  • + Notifications. Used only if you enable agent-status notifications or + Live Activities. +
  • +
  • + Local network. Used to discover or connect to T3 Code environments + accessible from your machine over the network. +
  • +
+
+ +
+

03

+

How We Use Your Personal Information

+

+ Subject to this Policy and applicable terms governing third-party applications and + services, we may use personal information for the following purposes: +

+
    +
  • To establish and verify your identity and authenticate your account;
  • +
  • To link and protect devices and environments and deliver optional notifications;
  • +
  • To process and fulfill requests you make through the Services;
  • +
  • To provide customer service and respond to support requests and feedback;
  • +
  • To communicate with you about the Services and your relationship with us;
  • +
  • To monitor reliability, prevent abuse, and investigate security incidents;
  • +
  • To comply with applicable law and enforce the terms governing the Services; and
  • +
  • + To analyze and improve T3 Code using aggregated, de-identified, or other anonymous + operational information. +
  • +
+ +

Training of AI Models

+

+ We do not use your personal information, source code, prompts, or agent responses to + train artificial-intelligence models. +

+ +

Compliance and Protection

+

We may use personal information to:

+
    +
  • Comply with applicable laws, lawful requests, and legal process;
  • +
  • Protect the rights, privacy, safety, and property of T3 Tools, our users, and others;
  • +
  • Audit our internal processes for legal, contractual, and policy compliance;
  • +
  • Enforce the terms and conditions that govern the Site and the Services; and
  • +
  • + Prevent, identify, investigate, and deter fraudulent, harmful, unauthorized, + unethical, or illegal activity, including cyberattacks and identity theft. +
  • +
+

+ We may also use personal information for other purposes consistent with this Policy or + explained to you when we collect the information. +

+
+ +
+

04

+

How We Share Your Personal Information

+

+ We may disclose the categories of personal information described above to the following + categories of third parties: +

+ +

Providers You Choose

+

+ At your direction, prompts, files, and related content are sent to coding agents and + artificial-intelligence providers configured in your environment. Because you bring and + configure your own coding-agent harness and provider, the applicable third parties vary. + Those providers process information under their own terms and privacy policies. +

+ +

Third-Party Service Providers

+

+ We use third-party service providers to help provide the Services and operate our + business. Depending on the features you use, these providers may include: +

+
    +
  • + Clerk (Clerk, Inc.).
    + We use Clerk to provide account authentication.
    + You can view Clerk's privacy policy here:{" "}https://clerk.com/legal/privacy. +
  • +
  • + Cloudflare (Cloudflare, Inc.).
    + We use Cloudflare for networking, T3 Connect infrastructure, and content delivery.
    + You can view Cloudflare's privacy policy here:{" "}https://www.cloudflare.com/policies/privacy/. +
  • +
  • + PlanetScale (PlanetScale, Inc.).
    + We use PlanetScale to host the database that supports T3 Connect.
    + You can view PlanetScale's privacy policy here:{" "}https://planetscale.com/legal/privacy. +
  • +
  • + Vercel (Vercel Inc.).
    + We use Vercel to host and deploy the T3 Code Site.
    + You can view Vercel's privacy notice here:{" "}https://vercel.com/legal/privacy-notice. +
  • +
  • + Axiom (Axiom, Inc.).
    + We use Axiom for operational diagnostics that help us maintain and troubleshoot the + Services.
    + You can view Axiom's privacy policy here:{" "}https://axiom.co/docs/legal/privacy. +
  • +
  • + Expo (650 Industries, Inc.).
    + We use Expo for application updates and related mobile-app services.
    + You can view Expo's privacy policy here:{" "}https://expo.dev/privacy. +
  • +
  • + Apple (Apple Inc.).
    + We use Apple services for app distribution, optional authentication, and platform + notifications.
    + You can view Apple's privacy policy here:{" "}https://www.apple.com/legal/privacy/. +
  • +
  • + Google (Google LLC).
    + We use Google services for app distribution and optional authentication.
    + You can view Google's privacy policy here:{" "}https://policies.google.com/privacy. +
  • +
+

+ These providers process information for us under their own contractual and privacy + obligations. +

+ +

Affiliates and Professional Advisors

+

+ We may share personal information with our affiliates for purposes consistent with this + Policy and with lawyers, accountants, bankers, and other professional advisors in the + course of the services they provide to us. +

+ +

Corporate Restructuring

+

+ We may share some or all personal information in connection with or during negotiation of + a merger, financing, acquisition, reorganization, bankruptcy, dissolution, sale of + assets, or similar transaction. If another company acquires T3 Tools or its assets, that + company may possess the personal information collected by us and assume the rights and + obligations described in this Policy. +

+ +

Legal and Safety Disclosures

+

+ We may disclose personal information if we believe in good faith that disclosure is + necessary to comply with law or legal process; protect or defend the rights, property, or + safety of T3 Tools, users of the Site or the Services, or others; investigate or prevent + fraud, abuse, or unlawful activity; or enforce this Policy and the terms governing the + Services. We may also share personal information at your direction, with your consent, or + as described to you when the information is collected. +

+ +

No Sale of Personal Information

+

+ We do not sell your personal information or share it for cross-context behavioral or + targeted advertising. +

+ +

Third-Party Websites and Services

+

+ The Site and the Services may contain links to third-party websites or services. When you + follow a third-party link or connect a third-party service, that third party may collect + personal information from you. We do not control and are not responsible for the privacy + practices or content of third parties. +

+
+ +
+

05

+

Your Choices Regarding Your Personal Information

+ +

Account and Connection Choices

+

+ You may use local or direct connections without signing in to a T3 account. You can + disconnect environments, sign out, disable optional notifications and Live Activities, + and clear local caches from Settings. You can grant or revoke camera, photo, local-network, + and notification permissions in your device settings. +

+ +

Cookies and Local Data

+

+ You can use your browser settings to stop accepting or to delete cookies, although some + portions of the Site or the Services may not function correctly as a result. Information + stored only on your device remains there until you clear it, remove the relevant + connection, or uninstall the app. The mobile app provides cache controls under + {" "}Settings → App → Client Storage. +

+ +

Retention and Deletion

+

+ We retain account, environment-link, device-registration, notification, and security + information while your account or the relevant feature is active and for as long as + reasonably necessary to provide the Services, protect them from abuse, comply with law, + and resolve disputes. Operational diagnostic traces are configured to expire after 30 + days. Some records may remain longer in backups or where retention is required for + security, fraud prevention, or legal compliance. +

+

+ To request deletion of your T3 account and associated T3 Connect data, email{" "}privacy@t3.tools{" "}from the email address associated with your account. We may need to verify your + identity. Deleting a T3 account does not delete information held by an environment or + coding-agent provider you control; contact those providers or delete that information + directly. +

+ +

Your Privacy Rights

+

+ Depending on where you live, you may have rights to request access to, correction of, + deletion of, or a portable copy of your personal information, or to restrict or object + to certain processing. You may also have the right to appeal our response or complain to + a data-protection authority. We will honor applicable rights after verifying your + request. +

+
+ +
+

06

+

Security of Your Personal Information

+

+ T3 Tools is committed to protecting the security of your personal information. We use + administrative, technical, and organizational safeguards designed to protect personal + information from unauthorized access, use, or disclosure. These safeguards include + encrypted network transport, scoped and proof-bound access tokens for supported + connection flows, protected credential storage, and redaction of authorization headers + from relay diagnostics. +

+

+ No method of transmission over the Internet or method of electronic storage is completely + secure. While we use reasonable efforts to protect personal information, we cannot + guarantee its absolute security. You are responsible for securing the environments, + coding-agent accounts, credentials, and networks that you connect to T3 Code. +

+
+ +
+

07

+

International Users

+

+ The Site and the Services are provided from the United States and are governed by United + States law. If you use the Site or the Services from outside the United States, your + personal information may be transferred to and processed in the United States and other + countries where we or our service providers operate. Where required, we use lawful + safeguards for international transfers of personal information. +

+
+ +
+

08

+

Children

+

+ The Site and the Services are not intended for children under 13 years of age, and you + must be at least 13 years old to use them. We do not knowingly collect, use, or disclose + personal information from children under 13. If you believe a child under 13 has provided + us with personal information, please contact us so that we can take appropriate action. +

+
+ +
+

09

+

Do Not Track

+

+ We currently do not respond to browser “Do Not Track” signals. T3 Code does not use + personal information for cross-context behavioral or targeted advertising. +

+
+ +
+

10

+

Updates to This Privacy Policy

+

+ We reserve the right to update this Policy from time to time. If we make material changes, + we will post the revised Policy at this URL, update the “Last Updated” and “Effective Date” + above, and provide any additional notice required by law. Except as otherwise indicated, + changes become effective when the revised Policy is posted. +

+
+ +
+

11

+

Contact Us

+

+ If you have questions about this Policy or wish to make a privacy or deletion request, + please contact us at: +

+
+ T3 Tools, Inc.
+ 2261 Market Street #5309
+ San Francisco, CA 94114
+ United States +
+ privacy@t3.tools +
+
diff --git a/apps/marketing/src/pages/security-policy.astro b/apps/marketing/src/pages/security-policy.astro new file mode 100644 index 000000000000..c9b05ec36fd4 --- /dev/null +++ b/apps/marketing/src/pages/security-policy.astro @@ -0,0 +1,187 @@ +--- +import LegalPage from "../components/LegalPage.astro"; + +const sections = [ + ["introduction", "Introduction"], + ["reporting", "Reporting security issues"], + ["safe-harbor", "Responsible research and safe harbor"], + ["practices", "Our security practices"], + ["responsibilities", "Your responsibilities"], + ["updates", "Updates to this policy"], + ["contact", "Contact us"], +] as const; +--- + + +
+

01

+

Introduction

+

+ This Security Policy describes the vulnerability-reporting process and security practices for + the T3 Code desktop and mobile applications, the hosted application at{" "}https://app.t3.codes, the optional T3 Connect service, and the T3 Code website at{" "}https://t3.codes. +

+

+ T3 Code connects software running on your devices and in environments you control with + coding-agent harnesses and providers you choose. We secure the software and infrastructure + operated by T3 Tools, but we do not control the security of your devices, environments, + repositories, networks, credentials, or third-party providers. Please review the shared + responsibilities below when assessing risk. +

+
+ +
+

02

+

Reporting Security Issues

+

+ If you believe you have found a security vulnerability affecting T3 Code or T3 Tools-operated + infrastructure, email{" "}security@ping.gg. Please do not + disclose the issue publicly until we have had a reasonable opportunity to investigate and + remediate it. +

+

Please include, when available:

+
    +
  • A description of the issue and its potential impact;
  • +
  • The affected application, version, URL, endpoint, or component;
  • +
  • Clear reproduction steps or a minimal proof of concept;
  • +
  • Relevant logs, screenshots, or request and response details with secrets removed; and
  • +
  • Your preferred contact information and whether you want public credit.
  • +
+

+ We aim to acknowledge complete reports within one business day, keep reporters informed of + material progress, and coordinate disclosure after a fix is available. Resolution time varies + with severity, complexity, and dependencies on third parties. +

+
+ +
+

03

+

Responsible Research and Safe Harbor

+

+ We support good-faith security research. If you make a genuine effort to comply with this + policy, avoid harm, respect privacy, and report findings promptly, we will treat your research + as authorized and will not initiate legal action against you for accidental, good-faith + violations of this policy. If a third party initiates legal action concerning compliant + research, we will make our authorization known where appropriate. +

+

To remain within this safe harbor, you must:

+
    +
  • Test only accounts, data, and systems you own or have explicit permission to test;
  • +
  • Stop and report immediately if you encounter personal, confidential, or production data;
  • +
  • Access only the minimum information needed to demonstrate the issue;
  • +
  • Avoid disrupting availability, degrading performance, or damaging or deleting data;
  • +
  • Not use social engineering, phishing, physical attacks, or denial-of-service testing;
  • +
  • Not test third-party services or infrastructure outside T3 Tools’ control; and
  • +
  • Give us reasonable time to address the issue before public disclosure.
  • +
+

+ This safe harbor does not authorize violations of law or activity outside the scope of this + policy. Vulnerability rewards are not guaranteed and, if offered, are determined by T3 Tools + in its discretion. +

+
+ +
+

04

+

Our Security Practices

+

+ We use administrative, technical, and organizational safeguards designed for the nature of T3 + Code and the information processed by T3 Tools-operated services. These practices include: +

+
    +
  • + Data minimization. T3 Code is designed so coding-session content can remain + between your clients, your environment, and the harnesses and providers you configure. We + process information through T3 Tools-operated infrastructure only when needed for a feature + you enable. +
  • +
  • + Encrypted transport. T3 Tools-operated network services use encrypted + transport. Supported connection flows use scoped credentials and security controls designed + to reduce unauthorized reuse. +
  • +
  • + Credential protection. T3 Code uses platform-provided protected storage for + credentials and session tokens where supported. We design operational logging to avoid + collecting secrets that are not needed to operate or troubleshoot the Services. +
  • +
  • + Access controls. Access to production systems and operational data is limited + according to job responsibilities and protected using authentication and authorization + controls. +
  • +
  • + Maintenance and monitoring. We monitor T3 Tools-operated services for + reliability and security events, review dependencies, and deploy updates and mitigations as + appropriate. +
  • +
  • + Established providers. We use specialized providers for services such as + authentication, hosting, and application distribution and evaluate the controls relevant to + their role. +
  • +
+

+ No system is completely secure. These practices reduce risk but do not guarantee that the Site + or Services will be free from vulnerabilities or unauthorized access. +

+
+ +
+

05

+

Your Responsibilities

+

+ Because you control much of the T3 Code execution path, you play an important role in securing + it. You are responsible for: +

+
    +
  • Keeping T3 Code, your operating systems, and connected tools up to date;
  • +
  • Securing your devices, repositories, environments, networks, and backups;
  • +
  • Protecting account, provider, repository, and environment credentials;
  • +
  • Using multi-factor authentication where your identity and coding-agent providers support it;
  • +
  • Granting providers and integrations only the permissions they need;
  • +
  • Reviewing commands, source changes, and other agent output before applying or executing it;
  • +
  • Removing lost or unused devices and revoking credentials you believe may be compromised; and
  • +
  • Following the security policies of your selected harnesses, providers, and infrastructure.
  • +
+

+ Do not send passwords, API keys, access tokens, or private keys in a vulnerability report. + Revoke any secret that may have been exposed before sharing sanitized evidence with us. +

+
+ +
+

06

+

Updates to This Policy

+

+ We may update this Security Policy as T3 Code, our infrastructure, and security practices + evolve. We will post revisions at this URL and update the dates above. Material changes to the + vulnerability-reporting process or safe-harbor terms will apply prospectively. +

+
+ +
+

07

+

Contact Us

+

+ Report security issues or ask security-related questions at{" "}security@ping.gg. +

+

+ For general legal questions, contact{" "}legal@t3.tools. + For privacy requests, contact{" "}privacy@t3.tools. +

+
+
diff --git a/apps/marketing/src/pages/terms-of-service.astro b/apps/marketing/src/pages/terms-of-service.astro new file mode 100644 index 000000000000..e6de5f2c751b --- /dev/null +++ b/apps/marketing/src/pages/terms-of-service.astro @@ -0,0 +1,396 @@ +--- +import LegalPage from "../components/LegalPage.astro"; + +const sections = [ + ["introduction", "Introduction"], + ["accounts", "Accounts"], + ["content", "Your content and providers"], + ["rights", "T3 Code and proprietary rights"], + ["acceptable-use", "Acceptable use"], + ["availability", "Availability and changes"], + ["fees", "Fees"], + ["privacy", "Privacy"], + ["termination", "Suspension and termination"], + ["beta", "Beta services"], + ["disclaimers", "Disclaimers"], + ["liability", "Limitation of liability"], + ["indemnification", "Indemnification"], + ["copyright", "Copyright complaints"], + ["third-parties", "Third-party services"], + ["feedback", "Feedback"], + ["disputes", "Dispute resolution"], + ["miscellaneous", "Miscellaneous"], + ["changes", "Changes to these Terms"], + ["contact", "Contact us"], +] as const; +--- + + +
+

01

+

Introduction

+

+ These Terms of Service (the “Terms”) are a binding agreement between you and T3 Tools, Inc. + (“T3 Tools,” “we,” “us,” or “our”). They govern your access to and use of the T3 Code desktop + and mobile applications, the hosted T3 Code application at{" "}https://app.t3.codes, and the optional T3 Connect service (together, the “Services”), as well as the T3 Code + marketing website at{" "}https://t3.codes (the “Site”). +

+

+ By accessing or using the Site or Services, you agree to these Terms. If you use the Services + for an organization, you represent that you have authority to bind that organization, and + “you” includes the organization. If you do not agree to these Terms, do not use the Site or + Services. +

+

+ You must be at least 13 years old to use the Site or Services. If the law where you live + requires a greater age to enter into these Terms, you must meet that requirement or have a + parent or legal guardian agree on your behalf. +

+
+ +
+

02

+

Accounts

+

+ You can use some T3 Code features without creating a T3 account. An account may be required + for account-backed features, including T3 Connect. You agree to provide accurate account + information, keep your credentials secure, and promptly notify us if you suspect unauthorized + access. You are responsible for activity performed through your account and connected devices, + except to the extent caused by our failure to use reasonable security measures. +

+

+ You may not share credentials in a way that compromises the Services, impersonate another + person, or create accounts through unauthorized automated means. We may require reasonable + verification before restoring access or acting on an account request. +

+
+ +
+

03

+

Your Content and Providers

+

+ T3 Code can help you send prompts, source files, images, attachments, review comments, and + instructions to coding-agent harnesses and providers you choose, and display their responses, + command output, file changes, source code, and diffs (collectively, “User Content”). You retain + all rights you have in your User Content. +

+

+ You bring and configure your own coding-agent harness and provider. Your User Content is + ordinarily processed in the environment you control and by the providers you select, not by + T3 Tools. When you enable a T3 Tools-operated feature that must transmit or process User + Content, you grant us a limited, non-exclusive license to host, transmit, reproduce, and + process that content only as reasonably necessary to provide, secure, and troubleshoot that + feature. This license ends when the content is no longer needed for those purposes, subject to + reasonable backup, security, and legal-retention requirements. +

+

+ You represent that you have the rights and permissions needed to use and submit User Content + and to direct its processing by your selected harnesses and providers. You are responsible for + reviewing those providers’ terms, privacy practices, data controls, and output before using + them with confidential, proprietary, regulated, or personal information. +

+
+ +
+

04

+

T3 Code and Proprietary Rights

+

+ T3 Tools and its licensors own the Site, the Services, and their branding, designs, hosted + infrastructure, documentation, and other materials, excluding User Content and third-party + materials. These Terms do not transfer ownership of either party’s intellectual property. +

+

+ Portions of T3 Code are available as open-source software. Your use, copying, modification, and + distribution of that source code are governed by the license included with the applicable + repository or component. These Terms govern your use of the hosted Services and do not limit + rights granted to you by an applicable open-source license. +

+

+ Subject to these Terms, we grant you a limited, non-exclusive, non-transferable, revocable right + to access and use the Services for lawful personal or internal business purposes. You may not + use our trademarks, service marks, or trade dress without written permission. +

+
+ +
+

05

+

Acceptable Use

+

You may not use the Site or Services to:

+
    +
  • Violate applicable law or another person’s rights;
  • +
  • Upload, transmit, or generate content you do not have the right to use;
  • +
  • Distribute malware, exploit code, or harmful content except in authorized security work;
  • +
  • Probe, attack, disrupt, or gain unauthorized access to the Services or another system;
  • +
  • Bypass rate limits, access controls, security measures, or usage restrictions;
  • +
  • Interfere with other users or place an unreasonable load on shared infrastructure;
  • +
  • Use the Services to facilitate fraud, harassment, abuse, or deceptive conduct; or
  • +
  • Misrepresent that output or activity from a third-party agent was produced or endorsed by us.
  • +
+

+ Authorized security research must follow our{" "}Security Policy. + We may investigate suspected violations and take proportionate action to protect users and the + Services. +

+
+ +
+

06

+

Availability and Changes

+

+ We may add, change, suspend, or discontinue features, integrations, or supported platforms. We + aim to provide reasonable notice when a change materially reduces an account-backed feature, + but urgent security, legal, or reliability changes may take effect immediately. The Services + may be unavailable because of maintenance, provider outages, network conditions, or events + outside our control. +

+

+ T3 Code depends on software, environments, networks, and coding-agent providers that you + configure or that third parties operate. We do not control their availability, compatibility, + pricing, or behavior. +

+
+ +
+

07

+

Fees

+

+ T3 Code is currently offered without a T3 Tools subscription fee unless we clearly state + otherwise for a feature. You remain responsible for charges from your coding-agent providers, + cloud infrastructure, network operators, app stores, and other third parties. If we introduce + a paid feature, we will present its price and applicable payment terms before you purchase it. +

+
+ +
+

08

+

Privacy

+

+ Our{" "}Privacy Policy explains how T3 Tools collects, uses, and + shares personal information when you use the Site and Services. Third-party environments, + harnesses, identity providers, and coding-agent providers process information under their own + terms and privacy policies. +

+
+ +
+

09

+

Suspension and Termination

+

+ You may stop using the Services at any time. You may disconnect environments and devices from + within T3 Code and may request deletion of an account as described in our Privacy Policy. +

+

+ We may suspend or terminate access to account-backed or hosted features if we reasonably + believe you materially violated these Terms, created a security or legal risk, failed to pay an + applicable fee, or used the Services in a way that could harm other users or shared + infrastructure. When practicable, we will give notice and an opportunity to cure. We may act + immediately where needed to prevent harm or comply with law. +

+

+ Provisions that by their nature should survive termination—including ownership, disclaimers, + limitations of liability, indemnification, and dispute terms—will survive. +

+
+ +
+

10

+

Beta Services

+

+ T3 Code is an early-stage product, and some or all features may be identified as alpha, beta, + preview, experimental, or pre-release (“Beta Services”). Beta Services may be incomplete, + change without notice, contain errors, or lose data. Do not rely on Beta Services as the sole + copy of important work, and review proposed commands and file changes before applying them. +

+
+ +
+

11

+

Disclaimers

+ +

+ Coding agents and artificial-intelligence systems can produce incorrect, insecure, incomplete, + or harmful output. You are responsible for reviewing output, commands, patches, and other + actions before relying on or executing them. We do not warrant that the Services or third-party + output will be uninterrupted, secure, accurate, error-free, or suitable for your purpose. +

+

+ Some jurisdictions do not allow certain warranty exclusions, so some of the foregoing may not + apply to you. +

+
+ +
+

12

+

Limitation of Liability

+ +

+ To the maximum extent permitted by law, our aggregate liability for all claims arising from or + relating to the Site, Services, or these Terms will not exceed the greater of $100 or the amount + you paid directly to T3 Tools for the Services during the 12 months before the event giving rise + to the claim. These limitations apply regardless of the legal theory and even if a remedy fails + of its essential purpose. They do not limit liability that cannot lawfully be limited. +

+
+ +
+

13

+

Indemnification

+

+ To the extent permitted by law, you will defend, indemnify, and hold harmless T3 Tools and its + affiliates, officers, employees, and agents from third-party claims, damages, losses, and + reasonable legal fees arising from your User Content, your use of the Site or Services in + violation of these Terms or law, or your infringement of another person’s rights. We will + promptly notify you of a covered claim and reasonably cooperate in its defense. You may not + settle a claim in a way that admits fault by or imposes obligations on us without our written + consent. +

+
+ + + +
+

15

+

Third-Party Services

+

+ The Services may connect to or display content from third-party coding agents, model providers, + identity providers, repositories, app stores, websites, and infrastructure. Your use of those + services is governed by their terms. We do not endorse or control third-party services and are + not responsible for their content, security, availability, output, or data practices. +

+

+ Third-party and open-source software included with T3 Code is governed by its applicable + license notices. Where those licenses conflict with these Terms for that software, the + applicable open-source license controls. +

+
+ +
+

16

+

Feedback

+

+ If you send us ideas, suggestions, or other feedback about T3 Code, you grant T3 Tools a + perpetual, irrevocable, worldwide, royalty-free right to use and commercialize that feedback + without restriction or compensation. This does not transfer ownership of your User Content. +

+
+ +
+

17

+

Dispute Resolution

+ +

Informal Resolution

+

+ Before starting arbitration or a court proceeding, the party raising a dispute must send a + written notice describing the dispute and requested relief. Notices to us must be sent to{" "}legal@t3.tools. The parties will try in good faith to resolve the dispute for at least 30 days. +

+

Binding Arbitration

+

+ Except for disputes eligible for small-claims court and requests for injunctive relief to + protect intellectual property or prevent unauthorized access, unresolved disputes will be + resolved by binding individual arbitration administered by JAMS under its applicable consumer + or comprehensive rules. The Federal Arbitration Act governs this agreement. Arbitration may + take place remotely unless the arbitrator determines that an in-person hearing is necessary. +

+

No Class Actions

+

+ Disputes must be brought only on an individual basis. Neither party may participate in a class, + collective, consolidated, or representative action or arbitration to the extent permitted by + law. +

+

Opt Out

+

+ You may opt out of this arbitration agreement by emailing{" "}legal@t3.tools within 30 days after you first accept these Terms. Include your name, the email associated + with your account if any, and a clear statement that you opt out of arbitration. +

+

+ If the arbitration agreement does not apply, exclusive jurisdiction and venue will lie in the + state and federal courts located in San Francisco County, California, and each party consents + to those courts. +

+
+ +
+

18

+

Miscellaneous

+

+ California law governs these Terms without regard to conflict-of-law principles, except that + the Federal Arbitration Act governs the arbitration provisions. These Terms and documents + incorporated by reference are the entire agreement between you and us regarding the Site and + Services. If a provision is unenforceable, it will be modified to the minimum extent necessary + or severed, and the remaining provisions will continue in effect. +

+

+ Our failure to enforce a provision is not a waiver. You may not assign these Terms without our + written consent. We may assign them in connection with a merger, acquisition, reorganization, + or sale of assets. We are not liable for delays or failures caused by events beyond our + reasonable control. Headings are for convenience only, and “including” means “including without + limitation.” +

+
+ +
+

19

+

Changes to These Terms

+

+ We may update these Terms from time to time. We will post updated Terms at this URL and update + the dates above. If a change materially affects your rights, we will provide additional notice + when reasonably practicable or as required by law. Changes apply prospectively from their + effective date. Your continued use of the Site or Services after that date means you accept the + updated Terms. +

+
+ +
+

20

+

Contact Us

+

If you have questions about these Terms, contact us at:

+
+ T3 Tools, Inc.
+ 2261 Market Street #5309
+ San Francisco, CA 94114
+ United States +
+ legal@t3.tools +
+
diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0bc0daecafc6..1f87560c8cad 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -5,7 +5,7 @@ "main": "index.ts", "scripts": { "dev": "expo start --clear", - "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --localhost", + "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan", "dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear --lan'", "start": "expo start", "start:dev": "APP_VARIANT=development expo start", diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 73c9ee9414ad..c2abb50c5598 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -44,8 +44,13 @@ import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppea import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen"; import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; +import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen"; import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen"; import { SettingsWaitlistRouteScreen } from "./features/settings/SettingsWaitlistRouteScreen"; +import { + SettingsLegalDocumentCloseHeaderButton, + SettingsLegalDocumentExternalHeaderButton, +} from "./features/settings/components/SettingsLegalDocumentRouteScreen"; import { useAppShortcuts } from "./features/shortcuts/useAppShortcuts"; import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; @@ -106,6 +111,14 @@ const SHEET_SOLID_HEADER_OPTIONS: AppScreenOptions = { unstable_navigationItemStyle: undefined, }; +const LEGAL_DOCUMENT_HEADER_OPTIONS: AppScreenOptions = { + ...SHEET_SOLID_HEADER_OPTIONS, + headerBackVisible: false, + headerLeft: SettingsLegalDocumentCloseHeaderButton, + headerRight: SettingsLegalDocumentExternalHeaderButton, + presentation: "fullScreenModal", +}; + const SettingsSheetStack = createNativeStackNavigator({ initialRouteName: "Settings", screenOptions: { @@ -239,6 +252,7 @@ const WORKSPACE_OVERLAY_ROUTES = new Set([ "GitConfirm", "GitOverview", "NewTaskSheet", + "SettingsLegal", "SettingsSheet", "ThreadReviewComment", ]); @@ -436,6 +450,14 @@ export const RootStack = createNativeStackNavigator({ }), }, }), + SettingsLegal: createNativeStackScreen({ + screen: SettingsLegalRouteScreen, + linking: "settings/legal", + options: { + ...LEGAL_DOCUMENT_HEADER_OPTIONS, + title: "Legal", + }, + }), ConnectOnboarding: createNativeStackScreen({ screen: ConnectOnboardingRouteScreen, linking: "connect-onboarding", diff --git a/apps/mobile/src/features/settings/SettingsLegalRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsLegalRouteScreen.tsx new file mode 100644 index 000000000000..7254186cf4c3 --- /dev/null +++ b/apps/mobile/src/features/settings/SettingsLegalRouteScreen.tsx @@ -0,0 +1,6 @@ +import { SettingsLegalDocumentRouteScreen } from "./components/SettingsLegalDocumentRouteScreen"; +import { LEGAL_URL } from "./lib/legal-document-url"; + +export function SettingsLegalRouteScreen() { + return ; +} diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 6f87eeeba844..6c67a4d89e8a 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -537,6 +537,7 @@ function AppSettingsSection() { return ( + navigation.goBack()} + className="p-2 active:opacity-60" + > + + + ); +} + +export function SettingsLegalDocumentExternalHeaderButton() { + const iconColor = useThemeColor("--color-icon"); + const route = useRoute>(); + const externalUrl = + route.params?.externalUrl && isLegalDocumentUrl(route.params.externalUrl) + ? route.params.externalUrl + : LEGAL_URL; + + return ( + void Linking.openURL(externalUrl).catch(() => undefined)} + className="p-2 active:opacity-60" + > + + + ); +} + +interface SettingsLegalDocumentRouteScreenProps { + readonly documentName: string; + readonly documentUrl: string; +} + +export function SettingsLegalDocumentRouteScreen({ + documentName, + documentUrl, +}: SettingsLegalDocumentRouteScreenProps) { + const navigation = useNavigation>(); + const iconColor = useThemeColor("--color-icon"); + const [reloadKey, setReloadKey] = useState(0); + const [loadProgress, setLoadProgress] = useState(0); + const [loadError, setLoadError] = useState(null); + + const openExternalUrl = useCallback((url: string) => { + void Linking.openURL(url).catch(() => undefined); + }, []); + if (loadError) { + return ( + + + + + Couldn't load the {documentName.toLowerCase()} + + + {loadError} + + + + { + setLoadError(null); + setReloadKey((value) => value + 1); + }} + className="items-center rounded-xl bg-foreground px-4 py-3 active:opacity-80" + > + Try Again + + openExternalUrl(documentUrl)} + className="items-center rounded-xl px-4 py-3 active:bg-foreground/5" + > + Open in Browser + + + + ); + } + + return ( + + {loadProgress > 0 && loadProgress < 1 ? : null} + { + if (isLegalDocumentUrl(request.url)) return true; + + openExternalUrl(request.url); + return false; + }} + onLoadProgress={(event) => { + setLoadProgress(event.nativeEvent.progress); + }} + onLoadStart={() => { + setLoadProgress(0.05); + setLoadError(null); + }} + onLoadEnd={(event) => { + if (isLegalDocumentUrl(event.nativeEvent.url)) { + navigation.setParams({ externalUrl: event.nativeEvent.url }); + } + setLoadProgress(0); + }} + onError={(event) => { + setLoadProgress(0); + setLoadError(event.nativeEvent.description || "The page could not be loaded."); + }} + onHttpError={(event) => { + if (!isLegalDocumentUrl(event.nativeEvent.url)) return; + setLoadProgress(0); + setLoadError(`The server returned status ${event.nativeEvent.statusCode}.`); + }} + renderLoading={() => ( + + + + )} + style={{ flex: 1, backgroundColor: "transparent" }} + /> + + ); +} diff --git a/apps/mobile/src/features/settings/components/SettingsRow.tsx b/apps/mobile/src/features/settings/components/SettingsRow.tsx index 7c3d9144e1ab..2f435c3a47f4 100644 --- a/apps/mobile/src/features/settings/components/SettingsRow.tsx +++ b/apps/mobile/src/features/settings/components/SettingsRow.tsx @@ -6,7 +6,7 @@ import { SymbolView } from "../../../components/AppSymbol"; import { AppText as Text } from "../../../components/AppText"; import { useThemeColor } from "../../../lib/useThemeColor"; -import type { SettingsSheetTarget } from "./settings-sheet-targets"; +import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets"; type SymbolName = ComponentProps["name"]; @@ -16,6 +16,7 @@ export function SettingsRow(props: { readonly label: string; readonly value?: string; readonly target?: SettingsSheetTarget; + readonly fullScreenTarget?: SettingsLegalDocumentTarget; readonly onPress?: () => void; }) { const navigation = useNavigation(); @@ -72,6 +73,20 @@ export function SettingsRow(props: { ); } + const fullScreenTarget = props.fullScreenTarget; + if (fullScreenTarget) { + return ( + navigation.navigate(fullScreenTarget)} + > + {content} + + ); + } + return ( {content} diff --git a/apps/mobile/src/features/settings/components/settings-sheet-targets.ts b/apps/mobile/src/features/settings/components/settings-sheet-targets.ts index 677dd5e4ed7b..71c059bedb48 100644 --- a/apps/mobile/src/features/settings/components/settings-sheet-targets.ts +++ b/apps/mobile/src/features/settings/components/settings-sheet-targets.ts @@ -3,3 +3,5 @@ export type SettingsSheetTarget = | "SettingsArchive" | "SettingsAppearance" | "SettingsClientStorage"; + +export type SettingsLegalDocumentTarget = "SettingsLegal"; diff --git a/apps/mobile/src/features/settings/lib/legal-document-url.test.ts b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts new file mode 100644 index 000000000000..10ce4ca4879a --- /dev/null +++ b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { isLegalDocumentUrl } from "./legal-document-url"; + +describe("isLegalDocumentUrl", () => { + it.each([ + "https://t3.codes/legal", + "https://t3.codes/legal/", + "https://t3.codes/privacy-policy?source=app", + "https://t3.codes/terms-of-service#updates", + "https://t3.codes/security-policy", + ])("allows a configured legal document: %s", (url) => { + expect(isLegalDocumentUrl(url)).toBe(true); + }); + + it.each([ + "https://t3.codes/download", + "https://example.com/legal", + "javascript:alert(1)", + "not-a-url", + ])("rejects a URL outside the legal-document allowlist: %s", (url) => { + expect(isLegalDocumentUrl(url)).toBe(false); + }); +}); diff --git a/apps/mobile/src/features/settings/lib/legal-document-url.ts b/apps/mobile/src/features/settings/lib/legal-document-url.ts new file mode 100644 index 000000000000..4556375615d0 --- /dev/null +++ b/apps/mobile/src/features/settings/lib/legal-document-url.ts @@ -0,0 +1,58 @@ +const DEFAULT_MARKETING_SITE_URL = "https://t3.codes"; + +function resolveMarketingSiteUrl(override: string | undefined): URL { + try { + const url = new URL(override?.trim() || DEFAULT_MARKETING_SITE_URL); + if (url.protocol !== "https:" && url.protocol !== "http:") { + return new URL(DEFAULT_MARKETING_SITE_URL); + } + + url.search = ""; + url.hash = ""; + url.pathname = `${url.pathname.replace(/\/+$/, "")}/`; + return url; + } catch { + return new URL(DEFAULT_MARKETING_SITE_URL); + } +} + +const MARKETING_SITE_URL = resolveMarketingSiteUrl(process.env.EXPO_PUBLIC_MARKETING_SITE_URL); + +function marketingSiteDocumentUrl(path: string): string { + return new URL(path, MARKETING_SITE_URL).toString(); +} + +export const PRIVACY_POLICY_URL = marketingSiteDocumentUrl("privacy-policy"); +export const SECURITY_POLICY_URL = marketingSiteDocumentUrl("security-policy"); +export const TERMS_OF_SERVICE_URL = marketingSiteDocumentUrl("terms-of-service"); +export const LEGAL_URL = marketingSiteDocumentUrl("legal"); + +export const ALLOWED_LEGAL_DOCUMENT_URLS = [ + LEGAL_URL, + PRIVACY_POLICY_URL, + TERMS_OF_SERVICE_URL, + SECURITY_POLICY_URL, +] as const; + +function webDocumentIdentity(value: string): string | null { + try { + const url = new URL(value); + if (url.protocol !== "https:" && url.protocol !== "http:") return null; + + const pathname = url.pathname.replace(/\/+$/, "") || "/"; + return `${url.origin}${pathname}`; + } catch { + return null; + } +} + +const ALLOWED_LEGAL_DOCUMENT_IDENTITIES = new Set( + ALLOWED_LEGAL_DOCUMENT_URLS.map(webDocumentIdentity).filter( + (value): value is string => value !== null, + ), +); + +export function isLegalDocumentUrl(value: string): boolean { + const identity = webDocumentIdentity(value); + return identity !== null && ALLOWED_LEGAL_DOCUMENT_IDENTITIES.has(identity); +} From ecb35f75839925dd1ac6f854efeef5c9e291d11b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 15 Jul 2026 12:48:55 +0200 Subject: [PATCH 06/19] Fix Legal modal header crash (#4000) Co-authored-by: codex --- apps/mobile/src/Stack.tsx | 2 +- .../SettingsLegalDocumentRouteScreen.tsx | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index c2abb50c5598..3837e5dde566 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -115,7 +115,7 @@ const LEGAL_DOCUMENT_HEADER_OPTIONS: AppScreenOptions = { ...SHEET_SOLID_HEADER_OPTIONS, headerBackVisible: false, headerLeft: SettingsLegalDocumentCloseHeaderButton, - headerRight: SettingsLegalDocumentExternalHeaderButton, + headerRight: () => , presentation: "fullScreenModal", }; diff --git a/apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx b/apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx index cf3bd7c804ad..aa5303b9a8a3 100644 --- a/apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx +++ b/apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx @@ -1,11 +1,5 @@ -import { - type NavigationProp, - type ParamListBase, - type RouteProp, - useNavigation, - useRoute, -} from "@react-navigation/native"; -import { useCallback, useState } from "react"; +import { type NavigationProp, type ParamListBase, useNavigation } from "@react-navigation/native"; +import { useCallback, useLayoutEffect, useState } from "react"; import { ActivityIndicator, Linking, Pressable, View } from "react-native"; import { WebView } from "react-native-webview"; @@ -15,10 +9,6 @@ import { SymbolView } from "../../../components/AppSymbol"; import { useThemeColor } from "../../../lib/useThemeColor"; import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url"; -type SettingsLegalRouteParams = { - SettingsLegal: { readonly externalUrl?: string } | undefined; -}; - export function SettingsLegalDocumentCloseHeaderButton() { const navigation = useNavigation(); const iconColor = useThemeColor("--color-icon"); @@ -42,20 +32,20 @@ export function SettingsLegalDocumentCloseHeaderButton() { ); } -export function SettingsLegalDocumentExternalHeaderButton() { +export function SettingsLegalDocumentExternalHeaderButton({ + externalUrl = LEGAL_URL, +}: { + readonly externalUrl?: string; +}) { const iconColor = useThemeColor("--color-icon"); - const route = useRoute>(); - const externalUrl = - route.params?.externalUrl && isLegalDocumentUrl(route.params.externalUrl) - ? route.params.externalUrl - : LEGAL_URL; + const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL; return ( void Linking.openURL(externalUrl).catch(() => undefined)} + onPress={() => void Linking.openURL(safeExternalUrl).catch(() => undefined)} className="p-2 active:opacity-60" > (null); + const [externalUrl, setExternalUrl] = useState(documentUrl); + const renderExternalHeaderButton = useCallback( + () => , + [externalUrl], + ); + + useLayoutEffect(() => { + navigation.setOptions({ + headerRight: renderExternalHeaderButton, + }); + }, [navigation, renderExternalHeaderButton]); const openExternalUrl = useCallback((url: string) => { void Linking.openURL(url).catch(() => undefined); @@ -160,7 +161,7 @@ export function SettingsLegalDocumentRouteScreen({ }} onLoadEnd={(event) => { if (isLegalDocumentUrl(event.nativeEvent.url)) { - navigation.setParams({ externalUrl: event.nativeEvent.url }); + setExternalUrl(event.nativeEvent.url); } setLoadProgress(0); }} From b98233702c88fd7a4312b231e68b089d5f3b0e80 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 16 Jul 2026 09:22:07 +0200 Subject: [PATCH 07/19] [codex] Fix onboarding connection status (#4001) Co-authored-by: codex --- .../cloud/CloudEnvironmentConnectList.tsx | 113 +++++++++++------- .../cloud/ConnectOnboardingDialog.tsx | 14 +-- ...dEnvironmentConnectionPresentation.test.ts | 55 +++++++++ .../cloudEnvironmentConnectionPresentation.ts | 58 +++++++++ .../settings/ConnectionsSettings.tsx | 14 +-- 5 files changed, 196 insertions(+), 58 deletions(-) create mode 100644 apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.test.ts create mode 100644 apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.ts diff --git a/apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx b/apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx index 7f2d63e7ad2d..460a253812a0 100644 --- a/apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx +++ b/apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx @@ -1,5 +1,6 @@ import { findErrorTraceId } from "@t3tools/client-runtime/errors"; import { + type EnvironmentConnectionPresentation, RelayConnectionRegistration, RelayConnectionTarget, } from "@t3tools/client-runtime/connection"; @@ -10,7 +11,7 @@ import { import type { EnvironmentId } from "@t3tools/contracts"; import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; import * as Option from "effect/Option"; -import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; import { environmentCatalog } from "~/connection/catalog"; import { cn } from "~/lib/utils"; @@ -22,6 +23,12 @@ import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "../settings/itemRo import { Button } from "../ui/button"; import { Skeleton } from "../ui/skeleton"; import { toastManager } from "../ui/toast"; +import { presentSavedCloudEnvironmentConnection } from "./cloudEnvironmentConnectionPresentation"; + +export interface SavedCloudEnvironmentConnection { + readonly environmentId: EnvironmentId; + readonly connection: EnvironmentConnectionPresentation; +} export function RemoteEnvironmentRowsSkeleton() { return ( @@ -40,19 +47,19 @@ export function RemoteEnvironmentRowsSkeleton() { /** * The user's T3 Connect environments from relay discovery, each with a * Connect button. The primary environment is always excluded; already-saved - * environments are hidden unless `showSavedAsConnected` renders them as - * connected instead (used by onboarding, where the full device mesh should be - * visible). + * environments are hidden unless `showSavedEnvironments` renders them with + * their live connection state (used by onboarding, where the full device mesh + * should be visible). */ export function CloudEnvironmentConnectRows({ primaryEnvironmentId, - savedEnvironmentIds, - showSavedAsConnected = false, + savedEnvironments, + showSavedEnvironments = false, empty = null, }: { readonly primaryEnvironmentId: EnvironmentId | null; - readonly savedEnvironmentIds: ReadonlyArray; - readonly showSavedAsConnected?: boolean; + readonly savedEnvironments: ReadonlyArray; + readonly showSavedEnvironments?: boolean; readonly empty?: ReactNode; }) { const environmentsState = useRelayEnvironmentDiscovery(); @@ -77,7 +84,9 @@ export function CloudEnvironmentConnectRows({ const [connectingEnvironmentId, setConnectingEnvironmentId] = useState( null, ); - const savedIds = useMemo(() => new Set(savedEnvironmentIds), [savedEnvironmentIds]); + const savedById = new Map( + savedEnvironments.map((environment) => [environment.environmentId, environment]), + ); useEffect(() => { void refreshRelayEnvironments(); @@ -90,8 +99,8 @@ export function CloudEnvironmentConnectRows({ if (result._tag === "Success") { toastManager.add({ type: "success", - title: "Environment connected", - description: `${environment.label} is available through T3 Connect.`, + title: "Environment added", + description: `Connecting to ${environment.label} through T3 Connect.`, }); return; } @@ -121,10 +130,10 @@ export function CloudEnvironmentConnectRows({ const visibleEnvironments = [...environmentsState.environments.values()].filter( ({ environment }) => environment.environmentId !== primaryEnvironmentId && - (showSavedAsConnected || !savedIds.has(environment.environmentId)), + (showSavedEnvironments || !savedById.has(environment.environmentId)), ); - const standalone = showSavedAsConnected || savedEnvironmentIds.length === 0; + const standalone = showSavedEnvironments || savedEnvironments.length === 0; if ( standalone && @@ -163,31 +172,57 @@ export function CloudEnvironmentConnectRows({ } return visibleEnvironments.map(({ environment, availability, error }) => { - const alreadyConnected = savedIds.has(environment.environmentId); + const savedEnvironment = savedById.get(environment.environmentId); + const savedConnection = savedEnvironment + ? presentSavedCloudEnvironmentConnection(savedEnvironment.connection) + : null; + const dotClassName = savedConnection + ? savedConnection.tone === "connected" + ? "bg-success" + : savedConnection.tone === "connecting" + ? "bg-warning" + : savedConnection.tone === "error" + ? "bg-destructive" + : "bg-muted-foreground/35" + : availability === "online" + ? "bg-success" + : availability === "error" + ? "bg-destructive" + : availability === "checking" + ? "bg-warning" + : "bg-muted-foreground/35"; + const statusText = savedConnection + ? savedConnection.statusText + : availability === "online" + ? "Available · Relay online" + : availability === "offline" + ? "Available · Relay offline" + : availability === "checking" + ? "Available · Checking relay status…" + : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable"); return (

{environment.label}

@@ -195,21 +230,19 @@ export function CloudEnvironmentConnectRows({

- {availability === "online" - ? "Available · Relay online" - : availability === "offline" - ? "Available · Relay offline" - : availability === "checking" - ? "Available · Checking relay status…" - : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable")} + {statusText}

- {alreadyConnected ? ( + {savedConnection ? ( ) : ( ) : ( @@ -250,7 +251,11 @@ export function CloudEnvironmentConnectRows({ disabled={connectingEnvironmentId !== null} onClick={() => void connectEnvironment(environment)} > - {connectingEnvironmentId === environment.environmentId ? "Connecting…" : "Connect"} + {connectingEnvironmentId === environment.environmentId + ? "Connecting…" + : savedConnection?.tone === "error" + ? "Retry" + : "Connect"} )}
From e3cd2ec8c612c671759af96b9fe13324b7baa5ff Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 16 Jul 2026 09:24:41 +0200 Subject: [PATCH 16/19] Isolate native diff highlight grammar state (#4029) (cherry picked from commit 5e8b2c800ecd98e23adbdbfc8970f4687ac8254b) --- .../diffs/nativeReviewDiffHighlighter.test.ts | 189 ++++++++++++++++++ .../diffs/nativeReviewDiffHighlighter.ts | 80 +++++++- 2 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.test.ts diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.test.ts b/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.test.ts new file mode 100644 index 000000000000..9e1480d1de93 --- /dev/null +++ b/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it } from "vite-plus/test"; + +import type { NativeReviewDiffRow } from "./nativeReviewDiffSurface"; +import type { NativeReviewDiffFile } from "./nativeReviewDiffTypes"; +import { highlightNativeReviewDiffVisibleRows } from "./nativeReviewDiffHighlighter"; + +const TYPESCRIPT_FILE: NativeReviewDiffFile = { + id: "file-1", + path: "example.ts", + language: "typescript", + additions: 0, + deletions: 0, +}; + +function makeLine( + input: Pick, +): NativeReviewDiffRow { + return { + kind: "line", + fileId: TYPESCRIPT_FILE.id, + ...input, + }; +} + +function makeHunk(id: string): NativeReviewDiffRow { + return { + kind: "hunk", + id, + fileId: TYPESCRIPT_FILE.id, + text: "@@", + }; +} + +function highlight( + rows: ReadonlyArray, + alreadyHighlightedRowIds?: ReadonlySet, +) { + return highlightNativeReviewDiffVisibleRows({ + rows, + files: [TYPESCRIPT_FILE], + scheme: "dark", + engine: "javascript", + firstRowIndex: 0, + lastRowIndex: rows.length - 1, + overscanRows: 0, + maxRows: 100, + alreadyHighlightedRowIds, + }); +} + +describe("highlightNativeReviewDiffVisibleRows", () => { + it("does not carry grammar state across hunk boundaries", async () => { + const exportRow = makeLine({ + id: "export-row", + content: "export async function run() {}", + change: "add", + oldLineNumber: null, + newLineNumber: 100, + }); + const rows = [ + makeHunk("hunk-1"), + makeLine({ + id: "import-open", + content: "import {", + change: "context", + oldLineNumber: 1, + newLineNumber: 1, + }), + makeLine({ + id: "import-entry", + content: " Model,", + change: "context", + oldLineNumber: 2, + newLineNumber: 2, + }), + makeHunk("hunk-2"), + exportRow, + ]; + + const [highlighted, standalone] = await Promise.all([ + highlight(rows), + highlight([makeHunk("standalone-hunk"), exportRow]), + ]); + + expect(highlighted.tokensByRowId[exportRow.id]).toEqual(standalone.tokensByRowId[exportRow.id]); + }); + + it("keeps grammar state across inline comment rows", async () => { + const openingRow = makeLine({ + id: "template-open", + content: "const message = `open", + change: "add", + oldLineNumber: null, + newLineNumber: 1, + }); + const closingRow = makeLine({ + id: "template-close", + content: "closed`;", + change: "add", + oldLineNumber: null, + newLineNumber: 2, + }); + const trailingRow = makeLine({ + id: "trailing-row", + content: "export const answer = 42;", + change: "add", + oldLineNumber: null, + newLineNumber: 3, + }); + const commentRow: NativeReviewDiffRow = { + kind: "comment", + id: "comment-1", + fileId: TYPESCRIPT_FILE.id, + commentText: "Review note", + }; + + const [withComment, contiguous] = await Promise.all([ + highlight([openingRow, commentRow, closingRow, trailingRow]), + highlight([openingRow, closingRow, trailingRow]), + ]); + + expect(withComment.tokensByRowId).toEqual(contiguous.tokensByRowId); + }); + + it("does not join unhighlighted rows across cached gaps", async () => { + const trailingRow = makeLine({ + id: "trailing-row", + content: "export const answer = 42;", + change: "add", + oldLineNumber: null, + newLineNumber: 3, + }); + const rows = [ + makeLine({ + id: "template-open", + content: "const message = `open", + change: "add", + oldLineNumber: null, + newLineNumber: 1, + }), + makeLine({ + id: "template-close", + content: "closed`;", + change: "add", + oldLineNumber: null, + newLineNumber: 2, + }), + trailingRow, + ]; + + const [highlighted, standalone] = await Promise.all([ + highlight(rows, new Set(["template-close"])), + highlight([trailingRow]), + ]); + + expect(highlighted.tokensByRowId[trailingRow.id]).toEqual( + standalone.tokensByRowId[trailingRow.id], + ); + }); + + it("keeps deletion grammar state out of addition rows", async () => { + const additionRow = makeLine({ + id: "addition-row", + content: "export const answer = 42;", + change: "add", + oldLineNumber: null, + newLineNumber: 1, + }); + const rows = [ + makeLine({ + id: "deletion-row", + content: "const removed = `open", + change: "delete", + oldLineNumber: 1, + newLineNumber: null, + }), + additionRow, + ]; + + const [highlighted, standalone] = await Promise.all([ + highlight(rows), + highlight([additionRow]), + ]); + + expect(highlighted.tokensByRowId[additionRow.id]).toEqual( + standalone.tokensByRowId[additionRow.id], + ); + }); +}); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts b/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts index 6c8c957f5410..14158e61c7d6 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts @@ -56,6 +56,11 @@ interface NativeReviewDiffLineRow extends NativeReviewDiffRow { readonly content: string; } +interface IndexedNativeReviewDiffLineRow { + readonly row: NativeReviewDiffLineRow; + readonly rowIndex: number; +} + export interface NativeReviewDiffTokenChunk { readonly chunkIndex: number; readonly fileId: string; @@ -308,6 +313,58 @@ function isHighlightableLineRow(row: NativeReviewDiffRow): row is NativeReviewDi return row.kind === "line" && typeof row.fileId === "string" && typeof row.content === "string"; } +function hasConsecutiveLineNumbers( + previous: number | null | undefined, + next: number | null | undefined, +): boolean { + return typeof previous === "number" && typeof next === "number" && next === previous + 1; +} + +function hasOnlyCommentRowsBetween( + rows: ReadonlyArray, + previousRowIndex: number, + nextRowIndex: number, +): boolean { + for (let rowIndex = previousRowIndex + 1; rowIndex < nextRowIndex; rowIndex += 1) { + if (rows[rowIndex]?.kind !== "comment") { + return false; + } + } + return true; +} + +function canShareGrammarContext( + previous: IndexedNativeReviewDiffLineRow, + next: IndexedNativeReviewDiffLineRow, + rows: ReadonlyArray, +): boolean { + if ( + next.row.fileId !== previous.row.fileId || + !hasOnlyCommentRowsBetween(rows, previous.rowIndex, next.rowIndex) + ) { + return false; + } + + if (previous.row.change === "delete" || next.row.change === "delete") { + return ( + previous.row.change !== "add" && + next.row.change !== "add" && + hasConsecutiveLineNumbers(previous.row.oldLineNumber, next.row.oldLineNumber) + ); + } + + if (previous.row.change === "add" || next.row.change === "add") { + return hasConsecutiveLineNumbers(previous.row.newLineNumber, next.row.newLineNumber); + } + + return ( + previous.row.change === "context" && + next.row.change === "context" && + hasConsecutiveLineNumbers(previous.row.oldLineNumber, next.row.oldLineNumber) && + hasConsecutiveLineNumbers(previous.row.newLineNumber, next.row.newLineNumber) + ); +} + function groupLineRowsByFileId(rows: ReadonlyArray) { const rowsByFileId = new Map(); for (const row of rows) { @@ -360,7 +417,7 @@ export async function highlightNativeReviewDiffVisibleRows( const maxRows = input.maxRows ?? NATIVE_REVIEW_DIFF_VISIBLE_MAX_ROWS; const startIndex = clampRowIndex(input.firstRowIndex - overscanRows, input.rows); const endIndex = clampRowIndex(input.lastRowIndex + overscanRows, input.rows); - const selectedRows: NativeReviewDiffLineRow[] = []; + const selectedRows: IndexedNativeReviewDiffLineRow[] = []; for ( let rowIndex = startIndex; @@ -374,12 +431,12 @@ export async function highlightNativeReviewDiffVisibleRows( !input.alreadyHighlightedRowIds?.has(row.id) && fileMap.has(row.fileId) ) { - selectedRows.push(row); + selectedRows.push({ row, rowIndex }); } } const tokensByRowId: Record> = {}; - let segmentRows: NativeReviewDiffLineRow[] = []; + let segmentRows: IndexedNativeReviewDiffLineRow[] = []; let segmentFile: NativeReviewDiffFile | undefined; const flushSegment = () => { @@ -389,27 +446,34 @@ export async function highlightNativeReviewDiffVisibleRows( return; } - const code = segmentRows.map((row) => row.content).join("\n"); + const code = segmentRows.map(({ row }) => row.content).join("\n"); const tokenLines = highlighter.tokenize(code, { lang: segmentFile.language, theme }); - segmentRows.forEach((row, rowIndex) => { + segmentRows.forEach(({ row }, rowIndex) => { tokensByRowId[row.id] = tokenLines[rowIndex] ?? makePlainTokenFallback(row); }); segmentRows = []; segmentFile = undefined; }; - for (const row of selectedRows) { + for (const selectedRow of selectedRows) { + const { row } = selectedRow; const file = fileMap.get(row.fileId); if (!file) { continue; } - if (segmentFile && segmentFile.id !== file.id) { + const previousRow = segmentRows.at(-1); + if ( + segmentFile && + (segmentFile.id !== file.id || + (previousRow !== undefined && + !canShareGrammarContext(previousRow, selectedRow, input.rows))) + ) { flushSegment(); } segmentFile = file; - segmentRows.push(row); + segmentRows.push(selectedRow); } flushSegment(); From 1ced949171cc75882852491abb7b9ba9534fff21 Mon Sep 17 00:00:00 2001 From: Ishan Date: Thu, 16 Jul 2026 13:04:33 +0530 Subject: [PATCH 17/19] Fix macOS fullscreen titlebar spacing (#4019) (cherry picked from commit 0418301de79bb7b6f82305236325cb9811615e9e) --- apps/desktop/src/ipc/DesktopIpcHandlers.ts | 2 + apps/desktop/src/ipc/channels.ts | 2 + apps/desktop/src/ipc/methods/window.test.ts | 21 ++++++++++ apps/desktop/src/ipc/methods/window.ts | 10 +++++ apps/desktop/src/preload.ts | 13 ++++++ apps/desktop/src/window/DesktopWindow.test.ts | 40 ++++++++++++++++++- apps/desktop/src/window/DesktopWindow.ts | 11 ++++- apps/web/src/components/AppSidebarLayout.tsx | 28 ++++++++++++- packages/contracts/src/ipc.ts | 2 + 9 files changed, 124 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/ipc/DesktopIpcHandlers.ts b/apps/desktop/src/ipc/DesktopIpcHandlers.ts index 9e7730240d17..c37b4b9d604a 100644 --- a/apps/desktop/src/ipc/DesktopIpcHandlers.ts +++ b/apps/desktop/src/ipc/DesktopIpcHandlers.ts @@ -36,6 +36,7 @@ import { getLocalEnvironmentBootstraps, getLocalEnvironmentBearerToken, getLogDir, + getWindowFullscreenState, listLogFiles, openLogDir, openExternal, @@ -52,6 +53,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers" yield* PreviewIpc.installPreviewEventForwarding(); yield* ipc.handleSync(getAppBranding); + yield* ipc.handleSync(getWindowFullscreenState); yield* ipc.handleSync(getLocalEnvironmentBootstraps); yield* ipc.handle(getLocalEnvironmentBearerToken); diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts index b822af4985f1..61de3444c23a 100644 --- a/apps/desktop/src/ipc/channels.ts +++ b/apps/desktop/src/ipc/channels.ts @@ -4,6 +4,8 @@ export const SET_THEME_CHANNEL = "desktop:set-theme"; export const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; export const MENU_ACTION_CHANNEL = "desktop:menu-action"; +export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state"; +export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state"; export const UPDATE_STATE_CHANNEL = "desktop:update-state"; export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; export const UPDATE_SET_CHANNEL_CHANNEL = "desktop:update-set-channel"; diff --git a/apps/desktop/src/ipc/methods/window.test.ts b/apps/desktop/src/ipc/methods/window.test.ts index dfb4f7cc6856..7a621a822cec 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -1,12 +1,17 @@ import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import type * as Electron from "electron"; + import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts"; import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts"; +import * as ElectronWindow from "../../electron/ElectronWindow.ts"; import { extractWslDistroFromEnvironmentId, getLocalEnvironmentBootstraps, + getWindowFullscreenState, resolveSelectedWslLinuxPath, resolveWslPickerDistro, } from "./window.ts"; @@ -187,3 +192,19 @@ describe("resolveSelectedWslLinuxPath", () => { ); }); }); + +describe("getWindowFullscreenState", () => { + it.effect("reads the current native window state", () => { + const window = { isFullScreen: () => true } as Electron.BrowserWindow; + + return Effect.gen(function* () { + assert.isTrue(yield* getWindowFullscreenState.handler()); + }).pipe( + Effect.provide( + Layer.mock(ElectronWindow.ElectronWindow)({ + currentMainOrFirst: Effect.succeed(Option.some(window)), + }), + ), + ); + }); +}); diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index 8fc26b4897a7..ae973b81b590 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -56,6 +56,16 @@ export const getAppBranding = DesktopIpc.makeSyncIpcMethod({ }), }); +export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({ + channel: IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL, + result: Schema.Boolean, + handler: Effect.fn("desktop.ipc.window.getWindowFullscreenState")(function* () { + const electronWindow = yield* ElectronWindow.ElectronWindow; + const window = yield* electronWindow.currentMainOrFirst; + return Option.isSome(window) && window.value.isFullScreen(); + }), +}); + export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({ channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL, result: Schema.Array(DesktopEnvironmentBootstrapSchema), diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 66ea2e10d543..48a50cafaf47 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -120,6 +120,19 @@ contextBridge.exposeInMainWorld("desktopBridge", { ipcRenderer.removeListener(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener); }; }, + getWindowFullscreenState: () => + ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true, + onWindowFullscreenStateChange: (listener) => { + const wrappedListener = (_event: Electron.IpcRendererEvent, fullscreen: unknown) => { + if (typeof fullscreen !== "boolean") return; + listener(fullscreen); + }; + + ipcRenderer.on(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener); + return () => { + ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener); + }; + }, getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL), setUpdateChannel: (channel) => ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel), diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 24cfb459d51c..01b21936eff7 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -29,7 +29,7 @@ import * as ElectronMenu from "../electron/ElectronMenu.ts"; import * as ElectronShell from "../electron/ElectronShell.ts"; import * as ElectronTheme from "../electron/ElectronTheme.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; -import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts"; +import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts"; import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts"; import * as DesktopWindow from "./DesktopWindow.ts"; import * as PreviewManager from "../preview/Manager.ts"; @@ -47,6 +47,7 @@ const environmentInput = { } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; function makeFakeBrowserWindow() { + const windowListeners = new Map void>(); const webContentsListeners = new Map void>(); const webContents = { copyImageAt: vi.fn(), @@ -67,10 +68,13 @@ function makeFakeBrowserWindow() { close: vi.fn(), focus: vi.fn(), isDestroyed: vi.fn(() => false), + isFullScreen: vi.fn(() => false), isMinimized: vi.fn(() => false), isVisible: vi.fn(() => true), loadURL: vi.fn(() => Promise.resolve()), - on: vi.fn(), + on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => { + windowListeners.set(eventName, listener); + }), once: vi.fn(), restore: vi.fn(), setBackgroundColor: vi.fn(), @@ -89,6 +93,7 @@ function makeFakeBrowserWindow() { send: webContents.send, setAutoHideCursor: window.setAutoHideCursor, webContentsListeners, + windowListeners, }; } @@ -348,6 +353,37 @@ describe("DesktopWindow", () => { }), ); + it.effect("publishes native macOS fullscreen changes to the renderer", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const enterFullscreen = fakeWindow.windowListeners.get("enter-full-screen"); + const leaveFullscreen = fakeWindow.windowListeners.get("leave-full-screen"); + if (!enterFullscreen || !leaveFullscreen) { + return yield* Effect.die("fullscreen listeners were not registered"); + } + + enterFullscreen(); + leaveFullscreen(); + assert.deepEqual(fakeWindow.send.mock.calls, [ + [WINDOW_FULLSCREEN_STATE_CHANNEL, true], + [WINDOW_FULLSCREEN_STATE_CHANNEL, false], + ]); + }).pipe(Effect.provide(layer)); + }), + ); + it.effect("recovers when the development renderer is temporarily unreachable", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index cd9c2b0db6fe..0848703914fd 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -15,7 +15,7 @@ import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; import * as ElectronShell from "../electron/ElectronShell.ts"; import * as ElectronTheme from "../electron/ElectronTheme.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; -import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts"; +import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts"; import * as PreviewManager from "../preview/Manager.ts"; const TITLEBAR_HEIGHT = 40; @@ -367,6 +367,15 @@ export const make = Effect.gen(function* () { window.setTitle(environment.displayName); }); + if (environment.platform === "darwin") { + window.on("enter-full-screen", () => { + window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true); + }); + window.on("leave-full-screen", () => { + window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false); + }); + } + let developmentLoadRetryIndex = 0; let developmentLoadRetryFiber: Fiber.Fiber | undefined; const clearDevelopmentLoadRetry = () => { diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 0f1a8f9d4297..455cc9199f64 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -1,5 +1,5 @@ import { useAtomValue } from "@effect/atom-react"; -import { useEffect, type CSSProperties, type ReactNode } from "react"; +import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; import { useNavigate } from "@tanstack/react-router"; import { isElectron } from "../env"; @@ -55,11 +55,35 @@ function SidebarControl() { export function AppSidebarLayout({ children }: { children: ReactNode }) { const navigate = useNavigate(); + const isMacosDesktop = isElectron && isMacPlatform(navigator.platform); + const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => { + const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState; + return isMacosDesktop && typeof getWindowFullscreenState === "function" + ? getWindowFullscreenState() + : false; + }); const macosWindowControlsStyle = - isElectron && isMacPlatform(navigator.platform) + isMacosDesktop && !isWindowFullscreen ? ({ "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } as CSSProperties) : undefined; + useEffect(() => { + if (!isMacosDesktop) return; + const bridge = window.desktopBridge; + if (!bridge) return; + const { getWindowFullscreenState, onWindowFullscreenStateChange } = bridge; + if ( + typeof getWindowFullscreenState !== "function" || + typeof onWindowFullscreenStateChange !== "function" + ) { + return; + } + + const unsubscribe = onWindowFullscreenStateChange(setIsWindowFullscreen); + setIsWindowFullscreen(getWindowFullscreenState()); + return unsubscribe; + }, [isMacosDesktop]); + useEffect(() => { const onMenuAction = window.desktopBridge?.onMenuAction; if (typeof onMenuAction !== "function") { diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index e5a49f7e34e0..42e32e27121c 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -994,6 +994,8 @@ export interface DesktopBridge { ) => Promise; openExternal: (url: string) => Promise; onMenuAction: (listener: (action: string) => void) => () => void; + getWindowFullscreenState: () => boolean; + onWindowFullscreenStateChange: (listener: (fullscreen: boolean) => void) => () => void; getUpdateState: () => Promise; setUpdateChannel: (channel: DesktopUpdateChannel) => Promise; checkForUpdate: () => Promise; From 41380db76d7e80fb142284a227cfc6d56407f9fc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 16 Jul 2026 10:39:13 +0200 Subject: [PATCH 18/19] sync: port upstream duplicate workspace roots guard (#3829) Cherry-pick of upstream d114e2772 (Prevent duplicate project workspace roots). Adds requireActiveProjectWorkspaceRootAbsent invariant to the decider, normalizeProjectPathForComparison/normalizeProjectPathForDispatch moved to packages/shared/src/path.ts, and sidebar project-grouping dedupe-by-physical-key logic (prefers the primary environment's member, then freshest updatedAt/createdAt, then highest id). apps/server/src/provider/Layers/CursorAdapter.test.ts is intentionally left unchanged: upstream's hunk there fixes an unrelated ACP-stdio session/cancel test behavior that has no equivalent in the fork's SDK-based Cursor adapter test harness, and is unrelated to the workspace-root invariant this commit otherwise ports. Co-authored-by: codex --- .../src/orchestration/commandInvariants.ts | 25 ++++ .../decider.projectScripts.test.ts | 111 ++++++++++++++++ apps/server/src/orchestration/decider.ts | 15 +++ apps/web/src/components/Sidebar.tsx | 3 +- apps/web/src/environmentGrouping.test.ts | 121 ++++++++++++++++++ apps/web/src/sidebarProjectGrouping.ts | 81 ++++++++++-- packages/client-runtime/src/state/projects.ts | 36 +----- packages/shared/src/path.ts | 29 +++++ 8 files changed, 379 insertions(+), 42 deletions(-) diff --git a/apps/server/src/orchestration/commandInvariants.ts b/apps/server/src/orchestration/commandInvariants.ts index f5ab794bce76..b59ded77f4f4 100644 --- a/apps/server/src/orchestration/commandInvariants.ts +++ b/apps/server/src/orchestration/commandInvariants.ts @@ -6,6 +6,7 @@ import type { ProjectId, ThreadId, } from "@t3tools/contracts"; +import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; import * as Effect from "effect/Effect"; import { OrchestrationCommandInvariantError } from "./Errors.ts"; @@ -71,6 +72,30 @@ export function requireProjectAbsent(input: { ); } +export function requireActiveProjectWorkspaceRootAbsent(input: { + readonly readModel: OrchestrationReadModel; + readonly command: OrchestrationCommand; + readonly workspaceRoot: string; + readonly exceptProjectId?: ProjectId; +}): Effect.Effect { + const normalizedWorkspaceRoot = normalizeProjectPathForComparison(input.workspaceRoot); + const existingProject = input.readModel.projects.find( + (project) => + project.deletedAt === null && + normalizeProjectPathForComparison(project.workspaceRoot) === normalizedWorkspaceRoot && + project.id !== input.exceptProjectId, + ); + if (existingProject === undefined) { + return Effect.void; + } + return Effect.fail( + invariantError( + input.command.type, + `Active project '${existingProject.id}' already exists for workspace root '${normalizedWorkspaceRoot}'.`, + ), + ); +} + export function requireThread(input: { readonly readModel: OrchestrationReadModel; readonly command: OrchestrationCommand; diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 64ba159c740b..a0c068407339 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -94,6 +94,117 @@ it.layer(NodeServices.layer)("decider project scripts", (it) => { }), ); + it.effect("rejects project.create for an active workspace root that already exists", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const readModel = yield* projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-existing"), + type: "project.created", + occurredAt: now, + commandId: CommandId.make("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.make("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-existing"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModelSelection: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); + + const failure = yield* Effect.flip( + decideOrchestrationCommand({ + command: { + type: "project.create", + commandId: CommandId.make("cmd-project-create-duplicate-root"), + projectId: asProjectId("project-duplicate-root"), + title: "Duplicate Project", + workspaceRoot: "/tmp/project/", + createdAt: now, + }, + readModel, + }), + ); + + expect(failure.message).toContain( + "Active project 'project-existing' already exists for workspace root '/tmp/project'.", + ); + }), + ); + + it.effect("rejects project.meta.update when moving onto another active workspace root", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const withFirstProject = yield* projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create-first"), + aggregateKind: "project", + aggregateId: asProjectId("project-first"), + type: "project.created", + occurredAt: now, + commandId: CommandId.make("cmd-project-create-first"), + causationEventId: null, + correlationId: CommandId.make("cmd-project-create-first"), + metadata: {}, + payload: { + projectId: asProjectId("project-first"), + title: "First", + workspaceRoot: "/tmp/project-first", + defaultModelSelection: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); + const readModel = yield* projectEvent(withFirstProject, { + sequence: 2, + eventId: asEventId("evt-project-create-second"), + aggregateKind: "project", + aggregateId: asProjectId("project-second"), + type: "project.created", + occurredAt: now, + commandId: CommandId.make("cmd-project-create-second"), + causationEventId: null, + correlationId: CommandId.make("cmd-project-create-second"), + metadata: {}, + payload: { + projectId: asProjectId("project-second"), + title: "Second", + workspaceRoot: "/tmp/project-second", + defaultModelSelection: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); + + const failure = yield* Effect.flip( + decideOrchestrationCommand({ + command: { + type: "project.meta.update", + commandId: CommandId.make("cmd-project-update-duplicate-root"), + projectId: asProjectId("project-second"), + workspaceRoot: "/tmp/project-first", + }, + readModel, + }), + ); + + expect(failure.message).toContain( + "Active project 'project-first' already exists for workspace root '/tmp/project-first'.", + ); + }), + ); + it.effect("emits user message and turn-start-requested events for thread.turn.start", () => Effect.gen(function* () { const now = "2026-01-01T00:00:00.000Z"; diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 9c95b42269d6..1730494ecc6b 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -12,6 +12,7 @@ import type * as PlatformError from "effect/PlatformError"; import { OrchestrationCommandInvariantError } from "./Errors.ts"; import { listThreadsByProjectId, + requireActiveProjectWorkspaceRootAbsent, requireProject, requireProjectAbsent, requireThread, @@ -111,6 +112,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, projectId: command.projectId, }); + yield* requireActiveProjectWorkspaceRootAbsent({ + readModel, + command, + workspaceRoot: command.workspaceRoot, + exceptProjectId: command.projectId, + }); return { ...(yield* withEventBase({ @@ -138,6 +145,14 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, projectId: command.projectId, }); + if (command.workspaceRoot !== undefined) { + yield* requireActiveProjectWorkspaceRootAbsent({ + readModel, + command, + workspaceRoot: command.workspaceRoot, + exceptProjectId: command.projectId, + }); + } const occurredAt = yield* nowIso; return { ...(yield* withEventBase({ diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index b5e7e780dace..b1c9f4fc6096 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3190,8 +3190,9 @@ export default function Sidebar() { return buildPhysicalToLogicalProjectKeyMap({ projects: orderedProjects, settings: projectGroupingSettings, + primaryEnvironmentId, }); - }, [orderedProjects, projectGroupingSettings]); + }, [orderedProjects, projectGroupingSettings, primaryEnvironmentId]); const projectPhysicalKeyByScopedRef = useMemo( () => new Map( diff --git a/apps/web/src/environmentGrouping.test.ts b/apps/web/src/environmentGrouping.test.ts index c66bf4977b21..2e4e9c7f3dd6 100644 --- a/apps/web/src/environmentGrouping.test.ts +++ b/apps/web/src/environmentGrouping.test.ts @@ -7,6 +7,10 @@ import { derivePhysicalProjectKey, resolveProjectGroupingMode, } from "./logicalProject"; +import { + buildPhysicalToLogicalProjectKeyMap, + buildSidebarProjectSnapshots, +} from "./sidebarProjectGrouping"; import type { Project } from "./types"; const primaryEnvironmentId = EnvironmentId.make("env-primary"); @@ -119,4 +123,121 @@ describe("environment grouping", () => { }), ).toBe("separate"); }); + + it("dedupes stale project rows with the same environment and workspace path", () => { + const duplicate = makeProject({ + id: ProjectId.make("project-duplicate"), + workspaceRoot: "/tmp/shared-repo/", + repositoryIdentity, + updatedAt: "2026-01-01T00:00:00.000Z", + }); + const primary = makeProject({ + id: ProjectId.make("project-primary"), + repositoryIdentity, + updatedAt: "2026-01-02T00:00:00.000Z", + }); + const remote = makeProject({ + id: ProjectId.make("project-remote"), + environmentId: remoteEnvironmentId, + workspaceRoot: "/tmp/shared-repo", + repositoryIdentity, + }); + + const snapshots = buildSidebarProjectSnapshots({ + projects: [primary, duplicate, remote], + settings: defaultGroupingSettings, + primaryEnvironmentId, + resolveEnvironmentLabel: (environmentId) => + environmentId === remoteEnvironmentId ? "remote" : "primary", + }); + + expect(snapshots).toHaveLength(1); + expect(snapshots[0]?.groupedProjectCount).toBe(2); + expect(snapshots[0]?.memberProjects.map((project) => project.id)).toEqual([ + primary.id, + remote.id, + ]); + }); + + it("prefers the fresher project row when duplicate stale rows are ordered first", () => { + const staleDuplicate = makeProject({ + id: ProjectId.make("project-stale"), + workspaceRoot: "/tmp/shared-repo/", + repositoryIdentity, + updatedAt: "2026-01-01T00:00:00.000Z", + }); + const canonical = makeProject({ + id: ProjectId.make("project-canonical"), + workspaceRoot: "/tmp/shared-repo", + repositoryIdentity, + updatedAt: "2026-01-02T00:00:00.000Z", + }); + + const snapshots = buildSidebarProjectSnapshots({ + projects: [staleDuplicate, canonical], + settings: defaultGroupingSettings, + primaryEnvironmentId, + resolveEnvironmentLabel: () => "primary", + }); + + expect(snapshots).toHaveLength(1); + expect(snapshots[0]?.memberProjects.map((project) => project.id)).toEqual([canonical.id]); + expect(snapshots[0]?.id).toBe(canonical.id); + }); + + it("dedupes stale project rows before logical grouping", () => { + const staleWithoutRepositoryIdentity = makeProject({ + id: ProjectId.make("project-stale"), + repositoryIdentity: null, + updatedAt: "2026-01-01T00:00:00.000Z", + }); + const canonical = makeProject({ + id: ProjectId.make("project-canonical"), + repositoryIdentity, + updatedAt: "2026-01-02T00:00:00.000Z", + }); + const remote = makeProject({ + id: ProjectId.make("project-remote"), + environmentId: remoteEnvironmentId, + repositoryIdentity, + }); + + const snapshots = buildSidebarProjectSnapshots({ + projects: [staleWithoutRepositoryIdentity, canonical, remote], + settings: defaultGroupingSettings, + primaryEnvironmentId, + resolveEnvironmentLabel: (environmentId) => + environmentId === remoteEnvironmentId ? "remote" : "primary", + }); + + expect(snapshots).toHaveLength(1); + expect(snapshots[0]?.projectKey).toBe(repositoryIdentity.canonicalKey); + expect(snapshots[0]?.memberProjects.map((project) => project.id)).toEqual([ + canonical.id, + remote.id, + ]); + }); + + it("routes duplicate physical project keys to the winning logical group", () => { + const staleWithoutRepositoryIdentity = makeProject({ + id: ProjectId.make("project-stale"), + repositoryIdentity: null, + updatedAt: "2026-01-01T00:00:00.000Z", + }); + const canonical = makeProject({ + id: ProjectId.make("project-canonical"), + repositoryIdentity, + updatedAt: "2026-01-02T00:00:00.000Z", + }); + + const physicalToLogicalKey = buildPhysicalToLogicalProjectKeyMap({ + projects: [staleWithoutRepositoryIdentity, canonical], + settings: defaultGroupingSettings, + primaryEnvironmentId, + }); + + expect(physicalToLogicalKey.get(derivePhysicalProjectKey(staleWithoutRepositoryIdentity))).toBe( + repositoryIdentity.canonicalKey, + ); + }); }); diff --git a/apps/web/src/sidebarProjectGrouping.ts b/apps/web/src/sidebarProjectGrouping.ts index 599d43143df2..7ca9370b5145 100644 --- a/apps/web/src/sidebarProjectGrouping.ts +++ b/apps/web/src/sidebarProjectGrouping.ts @@ -31,16 +31,77 @@ export interface SidebarProjectSnapshot extends Project { remoteEnvironmentLabels: readonly string[]; } +interface SidebarProjectGroupCandidate { + readonly logicalKey: string; + readonly project: Project; +} + +function getProjectFreshnessTime(project: Project): number { + const updatedAtTime = Date.parse(project.updatedAt); + if (Number.isFinite(updatedAtTime)) { + return updatedAtTime; + } + const createdAtTime = Date.parse(project.createdAt); + return Number.isFinite(createdAtTime) ? createdAtTime : 0; +} + +function shouldReplaceDuplicateMember(input: { + existingMember: Project; + candidateMember: Project; + primaryEnvironmentId: EnvironmentId | null; +}): boolean { + if ( + input.primaryEnvironmentId !== null && + input.existingMember.environmentId !== input.primaryEnvironmentId && + input.candidateMember.environmentId === input.primaryEnvironmentId + ) { + return true; + } + + const existingFreshness = getProjectFreshnessTime(input.existingMember); + const candidateFreshness = getProjectFreshnessTime(input.candidateMember); + if (candidateFreshness !== existingFreshness) { + return candidateFreshness > existingFreshness; + } + + return input.candidateMember.id > input.existingMember.id; +} + +function collectProjectWinnersByPhysicalKey(input: { + projects: ReadonlyArray; + settings: ProjectGroupingSettings; + primaryEnvironmentId: EnvironmentId | null; +}): Map { + const winnersByPhysicalKey = new Map(); + for (const project of input.projects) { + const logicalKey = deriveLogicalProjectKeyFromSettings(project, input.settings); + const physicalProjectKey = derivePhysicalProjectKey(project); + const existing = winnersByPhysicalKey.get(physicalProjectKey); + if (!existing) { + winnersByPhysicalKey.set(physicalProjectKey, { logicalKey, project }); + continue; + } + if ( + shouldReplaceDuplicateMember({ + existingMember: existing.project, + candidateMember: project, + primaryEnvironmentId: input.primaryEnvironmentId, + }) + ) { + winnersByPhysicalKey.set(physicalProjectKey, { logicalKey, project }); + } + } + return winnersByPhysicalKey; +} + export function buildPhysicalToLogicalProjectKeyMap(input: { projects: ReadonlyArray; settings: ProjectGroupingSettings; + primaryEnvironmentId: EnvironmentId | null; }): Map { const mapping = new Map(); - for (const project of input.projects) { - mapping.set( - derivePhysicalProjectKey(project), - deriveLogicalProjectKeyFromSettings(project, input.settings), - ); + for (const [physicalProjectKey, winner] of collectProjectWinnersByPhysicalKey(input)) { + mapping.set(physicalProjectKey, winner.logicalKey); } return mapping; } @@ -56,17 +117,17 @@ export function buildSidebarProjectSnapshots(input: { // legacy behavior. isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean; }): SidebarProjectSnapshot[] { + const winnersByPhysicalKey = collectProjectWinnersByPhysicalKey(input); const groupedMembers = new Map(); - for (const project of input.projects) { - const logicalKey = deriveLogicalProjectKeyFromSettings(project, input.settings); + for (const { logicalKey, project } of winnersByPhysicalKey.values()) { const member: SidebarProjectGroupMember = { ...project, physicalProjectKey: derivePhysicalProjectKey(project), environmentLabel: input.resolveEnvironmentLabel(project.environmentId), }; - const existing = groupedMembers.get(logicalKey); - if (existing) { - existing.push(member); + const existingMembers = groupedMembers.get(logicalKey); + if (existingMembers) { + existingMembers.push(member); } else { groupedMembers.set(logicalKey, [member]); } diff --git a/packages/client-runtime/src/state/projects.ts b/packages/client-runtime/src/state/projects.ts index 82a43350650f..31f5ce111991 100644 --- a/packages/client-runtime/src/state/projects.ts +++ b/packages/client-runtime/src/state/projects.ts @@ -3,16 +3,16 @@ import { isUncPath, isWindowsAbsolutePath, isWindowsDrivePath, + normalizeProjectPathForComparison, + normalizeProjectPathForDispatch, } from "@t3tools/shared/path"; +export { normalizeProjectPathForComparison, normalizeProjectPathForDispatch }; + const isWindowsPlatform = (platform: string): boolean => { return /^win(dows)?/i.test(platform); }; -function isRootPath(value: string): boolean { - return value === "/" || value === "\\" || /^[a-zA-Z]:[/\\]?$/.test(value); -} - function getAbsolutePathKind(value: string): "unix" | "windows" | null { if (isWindowsDrivePath(value) || isUncPath(value)) { return "windows"; @@ -23,20 +23,6 @@ function getAbsolutePathKind(value: string): "unix" | "windows" | null { return null; } -function trimTrailingPathSeparators(value: string): string { - if (value.length === 0 || isRootPath(value)) { - return value; - } - const trimmed = - getAbsolutePathKind(value) === "unix" - ? value.replace(/\/+$/g, "") - : value.replace(/[\\/]+$/g, ""); - if (trimmed.length === 0) { - return value; - } - return /^[a-zA-Z]:$/.test(trimmed) ? `${trimmed}\\` : trimmed; -} - function preferredPathSeparator(value: string): "/" | "\\" { const absolutePathKind = getAbsolutePathKind(value); if (absolutePathKind === "windows") return "\\"; @@ -108,10 +94,6 @@ export function isUnsupportedWindowsProjectPath(value: string, platform: string) return isWindowsAbsolutePath(value) && !isWindowsPlatform(platform); } -export function normalizeProjectPathForDispatch(value: string): string { - return trimTrailingPathSeparators(value.trim()); -} - export function resolveProjectPathForDispatch(value: string, cwd?: string | null): string { const trimmedValue = value.trim(); if (!isExplicitRelativePath(trimmedValue) || !cwd) { @@ -139,14 +121,6 @@ export function resolveProjectPathForDispatch(value: string, cwd?: string | null ); } -export function normalizeProjectPathForComparison(value: string): string { - const normalized = normalizeProjectPathForDispatch(value); - if (isWindowsDrivePath(normalized) || normalized.startsWith("\\\\")) { - return normalized.replaceAll("/", "\\").toLowerCase(); - } - return normalized; -} - export function findProjectByPath( projects: ReadonlyArray, candidatePath: string, @@ -198,7 +172,7 @@ export function ensureBrowseDirectoryPath(currentPath: string): string { } export function getBrowseParentPath(currentPath: string): string | null { - const trimmed = trimTrailingPathSeparators(currentPath); + const trimmed = normalizeProjectPathForDispatch(currentPath); const absolutePath = splitAbsolutePath(trimmed); if (absolutePath) { if (absolutePath.segments.length === 0) return null; diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts index 2bb2ca0238db..66887d3f2ec3 100644 --- a/packages/shared/src/path.ts +++ b/packages/shared/src/path.ts @@ -20,3 +20,32 @@ export function isExplicitRelativePath(value: string): boolean { value.startsWith("..\\") ); } + +function isRootPath(value: string): boolean { + return value === "/" || value === "\\" || /^[a-zA-Z]:[/\\]?$/.test(value); +} + +function trimTrailingPathSeparators(value: string): string { + if (value.length === 0 || isRootPath(value)) { + return value; + } + const trimmed = value.startsWith("/") + ? value.replace(/\/+$/g, "") + : value.replace(/[\\/]+$/g, ""); + if (trimmed.length === 0) { + return value; + } + return /^[a-zA-Z]:$/.test(trimmed) ? `${trimmed}\\` : trimmed; +} + +export function normalizeProjectPathForDispatch(value: string): string { + return trimTrailingPathSeparators(value.trim()); +} + +export function normalizeProjectPathForComparison(value: string): string { + const normalized = normalizeProjectPathForDispatch(value); + if (isWindowsDrivePath(normalized) || isUncPath(normalized)) { + return normalized.replaceAll("/", "\\").toLowerCase(); + } + return normalized; +} From 07612d449cf1195f00379d58f34d6196ab3754c3 Mon Sep 17 00:00:00 2001 From: aaditagrawal Date: Thu, 16 Jul 2026 15:19:06 +0530 Subject: [PATCH 19/19] fix(web): keep duplicate project refs for sidebar thread lookup --- apps/web/src/environmentGrouping.test.ts | 14 ++++++++++++++ apps/web/src/sidebarProjectGrouping.ts | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/web/src/environmentGrouping.test.ts b/apps/web/src/environmentGrouping.test.ts index 2e4e9c7f3dd6..2384712172ba 100644 --- a/apps/web/src/environmentGrouping.test.ts +++ b/apps/web/src/environmentGrouping.test.ts @@ -157,6 +157,11 @@ describe("environment grouping", () => { primary.id, remote.id, ]); + expect(snapshots[0]?.memberProjectRefs.map((ref) => ref.projectId)).toEqual([ + primary.id, + duplicate.id, + remote.id, + ]); }); it("prefers the fresher project row when duplicate stale rows are ordered first", () => { @@ -182,6 +187,10 @@ describe("environment grouping", () => { expect(snapshots).toHaveLength(1); expect(snapshots[0]?.memberProjects.map((project) => project.id)).toEqual([canonical.id]); + expect(snapshots[0]?.memberProjectRefs.map((ref) => ref.projectId)).toEqual([ + staleDuplicate.id, + canonical.id, + ]); expect(snapshots[0]?.id).toBe(canonical.id); }); @@ -216,6 +225,11 @@ describe("environment grouping", () => { canonical.id, remote.id, ]); + expect(snapshots[0]?.memberProjectRefs.map((ref) => ref.projectId)).toEqual([ + staleWithoutRepositoryIdentity.id, + canonical.id, + remote.id, + ]); }); it("routes duplicate physical project keys to the winning logical group", () => { diff --git a/apps/web/src/sidebarProjectGrouping.ts b/apps/web/src/sidebarProjectGrouping.ts index 7ca9370b5145..737f4e1c8f5d 100644 --- a/apps/web/src/sidebarProjectGrouping.ts +++ b/apps/web/src/sidebarProjectGrouping.ts @@ -170,6 +170,13 @@ export function buildSidebarProjectSnapshots(input: { remoteMembers.length > 0 && remoteMembers.every((member) => isDesktopLocal(member.environmentId)); + // Keep duplicate (non-winning) project ids in thread lookup refs so threads + // still attached to stale rows remain visible under the winning sidebar row. + const physicalKeysInGroup = new Set(members.map((member) => member.physicalProjectKey)); + const memberProjectRefs = input.projects + .filter((project) => physicalKeysInGroup.has(derivePhysicalProjectKey(project))) + .map((project) => scopeProjectRef(project.environmentId, project.id)); + result.push({ ...representative, projectKey: logicalKey, @@ -185,7 +192,7 @@ export function buildSidebarProjectSnapshots(input: { hasLocal && hasRemote ? "mixed" : hasRemote ? "remote-only" : "local-only", allRemoteMembersAreDesktopLocal, memberProjects: members, - memberProjectRefs: members.map((member) => scopeProjectRef(member.environmentId, member.id)), + memberProjectRefs, remoteEnvironmentLabels, }); }