Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c6b8bb8
feat(desktop): build macOS previews from a PR label (#8182)
t3dotgg Aug 25, 2026
5d76653
fix(web): thread jump hints no longer stick after a dictation paste (…
t3dotgg Aug 25, 2026
e67074f
fix(web): keep grouped project renames (#7831)
MohtashamMurshid Aug 25, 2026
082e6ea
feat(web): reveal chat file chips in the system file manager (#7140)
SunkenInTime Aug 25, 2026
994372b
fix(server): push no longer writes a feature branch's commits to its …
t3dotgg Aug 25, 2026
5041777
chore(deps): bump @clerk/electron to 0.0.37 (#8240)
t3dotgg Aug 26, 2026
badae6a
feat(server): fetch legacy model classification from a hosted manifes…
t3dotgg Aug 26, 2026
860caaa
chore(release): prepare v0.0.34
t3-code[bot] Aug 26, 2026
b0a0281
fix(desktop): let Clerk UI receive stable auth fixes (#8248)
t3dotgg Aug 26, 2026
3b86ef9
fix(app): un-settled threads return to the top of the list (#8231)
t3dotgg Aug 26, 2026
a3a8cbd
perf(ci): cut about a minute from every release (#8250)
t3dotgg Aug 26, 2026
dbe5ca2
merge: sync upstream through a3a8cbd60
github-actions[bot] Aug 26, 2026
88ba03b
fix(sync): stamp unsettledAt on worktree lifecycle test rows
github-actions[bot] Aug 26, 2026
adb0127
fix(sync): expect upstream migration 043 in fork ledger snapshots
github-actions[bot] Aug 26, 2026
630e86d
fix(web): isolate ChatMarkdown workspace-image tests from module reuse
github-actions[bot] Aug 26, 2026
5a20c90
fix(web): isolate terminal close-confirm tests from localApi reuse
github-actions[bot] Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/desktop-macos-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Desktop macOS Preview

on:
pull_request:
types: [labeled, synchronize, reopened]

permissions:
contents: read
pull-requests: write

concurrency:
group: desktop-macos-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build:
name: Build macOS Apple Silicon preview
if: >-
github.event.pull_request.head.repo.full_name == github.repository &&
contains(github.event.pull_request.labels.*.name, 'preview:mac') &&
(github.event.action != 'labeled' || github.event.label.name == 'preview:mac')
runs-on: blacksmith-12vcpu-macos-26
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: false

- name: Install desktop dependencies
run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts...

- name: Cache resource monitor
id: resource_monitor_cache
uses: actions/cache@v6
with:
path: native/resource-monitor/target/aarch64-apple-darwin/release/t3-resource-monitor
key: resource-monitor-aarch64-apple-darwin-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }}

- name: Setup Rust
if: steps.resource_monitor_cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin

- id: version
name: Set preview version and public configuration
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

base_version="$(node -p "require('./apps/desktop/package.json').version")"
preview_version="${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}"
node scripts/update-release-package-versions.ts "$preview_version"
cp .env.example .env

echo "version=$preview_version" >> "$GITHUB_OUTPUT"

- id: build
name: Build unsigned macOS DMG
shell: bash
env:
T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
PREVIEW_VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail

vp run dist:desktop:artifact \
--platform mac \
--target dmg \
--arch arm64 \
--build-version "$PREVIEW_VERSION" \
--verbose

shopt -s nullglob
dmg_files=(release/*.dmg)
if (( ${#dmg_files[@]} != 1 )); then
printf 'Expected one DMG, found %s.\n' "${#dmg_files[@]}" >&2
exit 1
fi
printf 'dmg_name=%s\n' "$(basename "${dmg_files[0]}")" >> "$GITHUB_OUTPUT"

- id: upload
name: Upload macOS DMG
uses: actions/upload-artifact@v7
with:
path: release/*.dmg
if-no-files-found: error
archive: false
overwrite: true
retention-days: 7

- name: Comment download link
uses: actions/github-script@v8
env:
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
DMG_NAME: ${{ steps.build.outputs.dmg_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PREVIEW_VERSION: ${{ steps.version.outputs.version }}
with:
script: |
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
if (pullRequest.head.sha !== process.env.HEAD_SHA) {
core.info("Skipping the outdated macOS preview comment.");
return;
}

const marker = "<!-- desktop-macos-preview -->";
const body = [
marker,
"### macOS preview",
"",
`[Download Apple Silicon DMG](${process.env.ARTIFACT_URL})`,
"",
`Version: ${process.env.PREVIEW_VERSION}`,
`Commit: ${process.env.HEAD_SHA.slice(0, 7)}`,
"",
"Unsigned build. Clear quarantine before opening:",
"```sh",
`xattr -d com.apple.quarantine ~/Downloads/${process.env.DMG_NAME}`,
"```",
"",
"The download requires GitHub access and expires after 7 days.",
].join("\n");

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
per_page: 100,
});
const existing = comments.find((comment) => comment.body?.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
});
}
39 changes: 30 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
tags:
- "v*.*.*"
- "!v*-nightly.*"

workflow_dispatch:
inputs:
channel:
Expand All @@ -25,6 +26,17 @@ on:
default: false
type: boolean

# Serialize nightlies (scheduled and manual) so overlapping runs cannot build
# the same commit twice or publish out of order. Stable tag releases get their
# own group so a nightly never blocks them. Running publishers are never
# canceled, and queue: max keeps every pending run instead of the default
# newest-wins single slot, so a queued stable tag can never be silently
# dropped. Queued nightlies with no new commits skip via check_changes.
concurrency:
group: release-${{ (github.event_name == 'schedule' || inputs.channel == 'nightly') && 'nightly' || 'stable' }}
cancel-in-progress: false
queue: max

permissions:
contents: read
id-token: none
Expand Down Expand Up @@ -202,8 +214,14 @@ jobs:

relay_public_config:
name: Resolve T3 Connect public config
needs: preflight
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
# Consumes only the commit SHA, not preflight's resolved version, so it runs
# alongside preflight instead of after it. The condition mirrors preflight's:
# check_changes is skipped on non-schedule events (skipped is neither failure
# nor success, so success() would be wrong here).
needs: [check_changes]
if: |
!failure() && !cancelled() &&
(github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true')
runs-on: ubuntu-24.04
timeout-minutes: 5
environment:
Expand All @@ -225,7 +243,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
ref: ${{ github.sha }}
sparse-checkout: |
/*
!/.repos/
Expand Down Expand Up @@ -298,15 +316,19 @@ jobs:
# machine. node-pty is N-API, so one binary works across all WSL Node versions.
build_wsl_node_pty:
name: Build WSL node-pty (linux-x64)
needs: [preflight]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
# Same gating as relay_public_config: only the commit SHA is needed, so this
# runs alongside preflight. See the condition comment there.
needs: [check_changes]
if: |
!failure() && !cancelled() &&
(github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true')
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
ref: ${{ github.sha }}
sparse-checkout: |
/*
!/.repos/
Expand Down Expand Up @@ -761,9 +783,8 @@ jobs:
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"

- name: Build web package
run: vp run --filter @t3tools/web build

# The t3 build task depends on @t3tools/web#build, so the web client is
# built (once) as part of this step.
- name: Build CLI package
run: vp run --filter t3 build

Expand Down
7 changes: 7 additions & 0 deletions .macroscope/check-run-agents/ui-consistency.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ The goal is not to minimize CSS or class counts at any cost. The goal is to put
- Do not treat a screenshot as proof of keyboard, overflow, scrollbar, responsive, or runtime-theme behavior. Pair visual evidence with source, computed-style, emitted-CSS, or interaction checks as appropriate.
- Be alert to shared primitive color indirection. When a primitive routes icon color through a CSS variable, ensure migrated contextual icons retain their intended tone, including pressed and disabled states.

## Environment routing in shared renderers

- A shared renderer that performs an environment-scoped action — a server RPC such as opening or revealing a file, an environment-gated capability check, or an OS-derived label — must resolve its target environment from explicit scope: the bound thread's `environmentId`, or an `environmentId` prop threaded from the owning surface. Never let it silently fall back to the globally active environment. Multi-environment surfaces (pull request panels, review annotations, cross-environment listings) can render content from environment B while environment A is active; a silent fallback sends B's paths to A's server and presents A's platform wording.
- When a call site cannot supply an explicit environment scope, suppress the environment-scoped actions at that call site rather than guessing. A hidden menu item is correct; an item that targets the wrong server is a concrete finding.
- Capability gating, action dispatch, and user-facing labels must all read from the same environment's server config that the action will execute against. Flag a renderer whose label derives from one environment while its RPC targets another.
- Flag new call sites of shared markdown, chip, or menu renderers that trigger environment actions without passing explicit scope, and flag new environment-action props whose default reintroduces an active-environment fallback.

## Change discipline

- Review the pull request's changed scope and directly affected consumers. Do not turn a focused PR into a demand for unrelated legacy cleanup.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.33",
"version": "0.0.34",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ describe("sortThreadsForListV2", () => {
]);
expect(sorted.map((thread) => thread.id)).toEqual(["newest", "middle", "oldest"]);
});

it("surfaces an un-settled thread at the top via its re-entry stamp", () => {
const sorted = sortThreadsForListV2([
{
id: "old-unsettled",
createdAt: "2026-06-01T08:00:00.000Z",
unsettledAt: "2026-06-01T13:00:00.000Z",
},
{ id: "newest", createdAt: "2026-06-01T12:00:00.000Z" },
{ id: "middle", createdAt: "2026-06-01T10:00:00.000Z" },
]);
expect(sorted.map((thread) => thread.id)).toEqual(["old-unsettled", "newest", "middle"]);
});
});

describe("buildThreadListV2Items", () => {
Expand Down
27 changes: 18 additions & 9 deletions apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
groupSortedThreadsByRecency,
shouldShowRecencySectionHeaders,
} from "@t3tools/client-runtime/state/thread-recency-groups";
import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
import {
activeThreadAnchorTimestampMs,
sortPinnedThreadsByOrderKey,
} from "@t3tools/client-runtime/state/thread-sort";
import type { EnvironmentId, ProjectId, ThreadLinkedPullRequest } from "@t3tools/contracts";
import { threadMatchesAttributeQuery } from "@t3tools/shared/threadAttributeSearch";

Expand Down Expand Up @@ -196,19 +199,25 @@ function firstValidTimestampMs(...candidates: ReadonlyArray<string | null | unde
}

/**
* v2 sort: static creation order, newest thread on top. Activity NEVER
* reorders the list — a row holds its position from open until settled, so
* the screen only moves at lifecycle transitions. Mirrors web's
* sortThreadsForSidebarV2.
* v2 sort: static order, newest anchor on top. Activity NEVER reorders the
* list — a row holds its position between lifecycle transitions. The anchor
* is creation time until an un-settle re-anchors it (see
* activeThreadAnchorTimestampMs), so an un-settled thread surfaces at the
* top instead of sinking back to its creation-order slot. Mirrors web's
* sortThreadsForSidebar.
*/
export function sortThreadsForListV2<T extends { readonly id: string; readonly createdAt: string }>(
threads: readonly T[],
): T[] {
export function sortThreadsForListV2<
T extends {
readonly id: string;
readonly createdAt: string;
readonly unsettledAt?: string | null | undefined;
},
>(threads: readonly T[]): T[] {
// .sort() on a copy, not .toSorted(): Hermes doesn't ship the ES2023
// change-by-copy array methods.
return [...threads].sort(
(left, right) =>
parseTimestampMs(right.createdAt) - parseTimestampMs(left.createdAt) ||
activeThreadAnchorTimestampMs(right) - activeThreadAnchorTimestampMs(left) ||
left.id.localeCompare(right.id),
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "t3",
"version": "0.0.33",
"version": "0.0.34",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
20 changes: 16 additions & 4 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,17 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
const settledRows = yield* sql<{
readonly settledOverride: string | null;
readonly settledAt: string | null;
readonly unsettledAt: string | null;
}>`
SELECT
settled_override AS "settledOverride",
settled_at AS "settledAt"
settled_at AS "settledAt",
unsettled_at AS "unsettledAt"
FROM projection_threads
WHERE thread_id = 'thread-1'
`;
assert.deepEqual(settledRows, [
{ settledOverride: "settled", settledAt: "2026-01-01T00:00:01.000Z" },
{ settledOverride: "settled", settledAt: "2026-01-01T00:00:01.000Z", unsettledAt: null },
]);

yield* eventStore.append({
Expand All @@ -301,14 +303,24 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
const unsettledRows = yield* sql<{
readonly settledOverride: string | null;
readonly settledAt: string | null;
readonly unsettledAt: string | null;
}>`
SELECT
settled_override AS "settledOverride",
settled_at AS "settledAt"
settled_at AS "settledAt",
unsettled_at AS "unsettledAt"
FROM projection_threads
WHERE thread_id = 'thread-1'
`;
assert.deepEqual(unsettledRows, [{ settledOverride: "active", settledAt: null }]);
// The un-settle stamps the active-list re-entry time so clients can
// surface the thread at the top of the list.
assert.deepEqual(unsettledRows, [
{
settledOverride: "active",
settledAt: null,
unsettledAt: "2026-01-01T00:00:02.000Z",
},
]);
}),
);
});
Expand Down
Loading
Loading