Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
222 changes: 209 additions & 13 deletions .github/workflows/desktop-macos-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,39 @@ name: Desktop macOS Preview

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

permissions:
contents: read
pull-requests: write

# Build events and cleanup events use separate groups: a push must cancel a
# stale in-flight build, but must never cancel a cleanup run mid-delete. The
# publish job re-checks PR state before uploading to cover the reverse race.
concurrency:
group: desktop-macos-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
group: desktop-macos-preview-${{ github.event.pull_request.number }}-${{ contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && 'cleanup' || 'build' }}
# Cleanup runs must complete (a close event right after an unlabel queues
# behind the running cleanup instead of canceling it mid-delete), and events
# that skip the build job, such as adding an unrelated label, must not
# cancel an in-flight build either.
cancel-in-progress: ${{ !contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') }}

jobs:
# Builds run PR code, so this job keeps a read-only token. Publishing to the
# release happens in the publish job below, which never checks out PR code.
build:
name: Build macOS Apple Silicon preview
if: >-
github.event.action != 'closed' &&
github.event.action != 'unlabeled' &&
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')
# GitHub-hosted Apple Silicon runner; the fork has no Blacksmith macOS pool.
runs-on: macos-15
timeout-minutes: 30
outputs:
dmg_name: ${{ steps.build.outputs.dmg_name }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -94,8 +107,9 @@ jobs:
fi
printf 'dmg_name=%s\n' "$(basename "${dmg_files[0]}")" >> "$GITHUB_OUTPUT"

- id: upload
name: Upload macOS DMG
# archive: false uploads the file as its own artifact named after the
# file, so the publish job downloads by *.dmg pattern, not by name.
- name: Upload macOS DMG
uses: actions/upload-artifact@v7
with:
path: release/*.dmg
Expand All @@ -104,21 +118,124 @@ jobs:
overwrite: true
retention-days: 7

# Release assets download without a GitHub account, unlike workflow
# artifacts. All preview DMGs live on one rolling prerelease tagged
# "desktop-preview" (release.yml only matches v*.*.* tags), so publishing a
# build never notifies release watchers. This job holds the write token and
# only handles the artifact the build job produced; it never runs PR code.
publish:
name: Publish anonymous download
needs: build
runs-on: halifax-bl4ckbl1zz-t3code-db2e22d1
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Download macOS DMG
uses: actions/download-artifact@v8
with:
pattern: "*.dmg"
merge-multiple: true
path: release

- id: upload
name: Upload DMG to the rolling preview release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail

tag="desktop-preview"

# True while the PR is open and still carries the preview label.
preview_eligible() {
[[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--json state,labels \
--jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]]
}

# The build ran for many minutes. If the PR closed or lost the label
# meanwhile, cleanup already ran in its own concurrency group, so
# publishing now would resurrect a deleted download.
if ! preview_eligible; then
echo "PR closed or preview label removed while building. Skipping publish."
exit 0
fi

dmg_path="$(find release -type f -name '*.dmg' -print -quit)"
if [[ -z "$dmg_path" ]]; then
echo "No DMG found in the downloaded artifact." >&2
exit 1
fi

# The filename comes out of the build, which runs PR code. Requiring
# this PR's marker keeps a build from clobbering or deleting another
# PR's asset, since those names carry a different -pr.N. marker.
if [[ "$(basename "$dmg_path")" != *"-pr.${PR_NUMBER}."* ]]; then
echo "DMG name '$(basename "$dmg_path")' does not carry this PR's -pr.${PR_NUMBER}. marker. Refusing to publish." >&2
exit 1
fi

if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
# "|| true" tolerates a concurrent publish job creating the
# release between the check and the create.
gh release create "$tag" \
--repo "$GITHUB_REPOSITORY" \
--target "$DEFAULT_BRANCH" \
--prerelease \
--title "Desktop preview builds" \
--notes "Rolling unsigned desktop builds from pull requests with a preview label. Each download is removed when its pull request closes or loses the label. Install stable builds from the latest release instead." \
|| true
fi

# Keep one DMG per PR: drop this PR's older builds first. The
# trailing dot keeps -pr.12. from matching -pr.123. builds.
gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \
| { grep -F -- "-pr.${PR_NUMBER}." || true; } \
| while read -r asset; do
gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \
|| echo "Asset $asset was already removed by a concurrent run."
done

gh release upload "$tag" "$dmg_path" --repo "$GITHUB_REPOSITORY" --clobber

# Re-check after uploading. A cleanup run that started during the
# upload listed assets before ours existed, so it cannot delete it.
# Whichever writer acts last sees the final PR state; if the preview
# became ineligible, delete what we just uploaded.
if ! preview_eligible; then
gh release delete-asset "$tag" "$(basename "$dmg_path")" --repo "$GITHUB_REPOSITORY" --yes \
|| echo "Asset was already removed by a concurrent run."
echo "PR closed or preview label removed during upload. Removed the download."
exit 0
fi

echo "download_url=https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/$(basename "$dmg_path")" >> "$GITHUB_OUTPUT"

- name: Comment download link
if: steps.upload.outputs.download_url != ''
uses: actions/github-script@v8
env:
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
DMG_NAME: ${{ steps.build.outputs.dmg_name }}
DOWNLOAD_URL: ${{ steps.upload.outputs.download_url }}
DMG_NAME: ${{ needs.build.outputs.dmg_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PREVIEW_VERSION: ${{ steps.version.outputs.version }}
PREVIEW_VERSION: ${{ needs.build.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) {
if (
pullRequest.head.sha !== process.env.HEAD_SHA ||
pullRequest.state !== "open" ||
!pullRequest.labels.some((label) => label.name === "preview:mac")
) {
core.info("Skipping the outdated macOS preview comment.");
return;
}
Expand All @@ -128,7 +245,7 @@ jobs:
marker,
"### macOS preview",
"",
`[Download Apple Silicon DMG](${process.env.ARTIFACT_URL})`,
`[Download Apple Silicon DMG](${process.env.DOWNLOAD_URL})`,
"",
`Version: ${process.env.PREVIEW_VERSION}`,
`Commit: ${process.env.HEAD_SHA.slice(0, 7)}`,
Expand All @@ -138,10 +255,10 @@ jobs:
`xattr -d com.apple.quarantine ~/Downloads/${process.env.DMG_NAME}`,
"```",
"",
"The download requires GitHub access and expires after 7 days.",
"No GitHub sign-in is needed. The download stays available until this PR closes or the preview label is removed.",
].join("\n");

const { data: comments } = await github.rest.issues.listComments({
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
Expand All @@ -164,3 +281,82 @@ jobs:
body,
});
}

# The way out: closing the PR or removing the label deletes its DMG from the
# rolling release and updates the PR comment to say so.
cleanup:
name: Remove preview download
if: >-
github.event.pull_request.head.repo.full_name == github.repository &&
((github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview:mac')) ||
(github.event.action == 'unlabeled' && github.event.label.name == 'preview:mac'))
runs-on: halifax-bl4ckbl1zz-t3code-db2e22d1
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- id: delete
name: Delete this PR's preview assets
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

tag="desktop-preview"

# A stale cleanup must not delete a download that became valid
# again. If the PR is open and labeled once more, the next publish
# owns this PR's assets and replaces them itself.
if [[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--json state,labels \
--jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]]; then
echo "PR is open and labeled again. Skipping cleanup."
echo "removed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "removed=true" >> "$GITHUB_OUTPUT"

if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "No preview release exists. Nothing to clean up."
exit 0
fi

gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \
| { grep -F -- "-pr.${PR_NUMBER}." || true; } \
| while read -r asset; do
gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \
|| echo "Asset $asset was already removed by a concurrent run."
done

- name: Mark the preview comment as removed
if: steps.delete.outputs.removed == 'true'
uses: actions/github-script@v8
with:
script: |
const marker = "<!-- desktop-macos-preview -->";
const comments = await github.paginate(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) {
return;
}

await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: [
marker,
"### macOS preview",
"",
"The preview download was removed because this PR closed or the preview label was removed.",
].join("\n"),
});
37 changes: 36 additions & 1 deletion PATCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ This fork stays close to `pingdotgg/t3code` and carries only the following opera
- Keeps upstream's `EnvironmentProviderSettings` inline in
`apps/web/src/components/settings/SettingsPanels.tsx`; the fork carries no
`ProviderSettingsPanel.tsx`. Upstream changes to that file resolve to the fork: port the behavior
into `SettingsPanels.tsx` instead of restoring the module.
into `SettingsPanels.tsx` instead of restoring the module. Upstream's "split provider settings
into list and editor" (`e2d4d12a81`) is therefore not carried: it is a master-detail redesign of
that panel plus a `mode: "list" | "editor"` rewrite of `ProviderInstanceCard`, written against
the module the fork retired, and the fork's stacked collapsible list already reaches every field.
Carried out of it: `providerStatus.ts`'s neutral disabled dot (amber read as a warning on a
provider the user turned off) and `ProviderEnvironmentSection`'s draft re-sync, which adopts an
environment the server changed underneath a mounted card while ignoring echoes of what the
editor itself just published. `providerSettingsTabs.ts` has no fork counterpart.
- Runs the shared settle rules (`packages/client-runtime/src/state/threadSettled.ts`) against the
fork's orchestration V2 thread shell. Upstream types them on `OrchestrationThreadShell` and reads
`latestTurn`; the fork uses structural shapes (`QueuedThreadShell`/`SettlementThreadShell`,
Expand Down Expand Up @@ -153,6 +160,29 @@ This fork stays close to `pingdotgg/t3code` and carries only the following opera
`isNonRetryableProviderTurnControlFailure` succeeds the outbox item on "not active" races,
`ProviderSessionManager.detach` tolerates a failing `interruptTurn`, and
`ProviderRuntimeRecoveryService.recover` settles runs orphaned by a dead runtime.
- Ports the reachable half of upstream's "improve Grok skills, plans, usage, and turn reliability"
(`ead4ce52a1`) onto orchestration V2. Everything that lives in shared modules is carried as-is:
Grok skill discovery (`provider/Drivers/GrokSkills.ts`, wired through `checkGrokProviderStatus`,
which now takes the server `cwd`), the reasoning-effort model descriptors in `GrokProvider.ts`,
the `_meta`-carrying `session/set_model` path in `GrokAcpSupport.ts`, per-runtime-mode Grok spawn
arguments (`grokAcpSpawnArgs`), the plan-mode helpers and rate-limit/error prompt settlement in
`XAiAcpExtension.ts`, and the whole usage-transcript half. Two V2 seams were added to reach it:
`AcpAdapterV2RuntimeInput` now carries `runtimeMode` so Grok spawns with the thread's permission
mode, and the flavor gained `applySessionModel` / `modelOptionIdsHandledBySessionModel` so Grok's
`reasoningEffort` rides `session/set_model` `_meta` instead of `session/set_config_option` — the
fork's `configureSession` would otherwise reject the option the session never advertises. Not
carried: the commit's rewrite of the retired V1 `provider/Layers/GrokAdapter.ts` — its turn/active-tool
inactivity watchdog, the `enter_plan_mode`/`exit_plan_mode` proposed-plan gate, and
`selectGrokPermissionOptionId`'s allow*once fallback for "Always allow this session". The plan and
rate-limit helpers those used are present in `XAiAcpExtension.ts` but `GrokAdapterV2` registers no
`x.ai/exit_plan_mode` handler yet, so Grok plan mode still reaches the fork's clients as ordinary
tool calls. Upstream's mock-agent hooks for the watchdog (`T3_ACP_EMIT*\*\_THEN_HANG`) are carried so
a later port has its harness.
- Does not carry upstream's "recover stale Codex approval callbacks" (`230c5d4a5c`). It widens the
V1 `ProviderCommandReactor`'s "unknown pending approval request" matcher, and the fork deleted that
reactor with the V1 thread runtime. Orchestration V2 answers approvals through
`RuntimeRequestService` against `CodexAdapterV2`'s in-process deferred map rather than the V1
`ProviderService` callback registry, so the stale-callback shape has no fork counterpart to match.
- Does not carry upstream's V1 subagent-model buffering (`6a2608292d`) or its routine-event
projection skip (`c034f51bb7`). Both edit modules the fork deleted with the V1 thread runtime
(`provider/Layers/ClaudeAdapter.ts`, the thread half of
Expand Down Expand Up @@ -230,6 +260,11 @@ This fork stays close to `pingdotgg/t3code` and carries only the following opera
modules they cover, and `apps/server/src/ws.test.ts` holds the `server.getConfig`
discovery-timeout cases. Upstream additions to `server.test.ts` need rehoming rather than
merging, and its `Layer.mock(ExternalLauncher)` fixtures have no fork counterpart to update.
- Has no `apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts` either; it went with the
V1 thread projectors. The pipeline itself survives for the project aggregate, so upstream fixes to
it still apply and their tests get rehomed: upstream's "replay all un-applied events during
projection bootstrap" (`a6797b3b97`) is carried, and its backlog test lives in the fork's
`ProjectionPipelineBootstrap.test.ts` over `project.created` events alone.
- Fetches the provider model manifest (upstream `badae6a5cc`) from **upstream's** `main`
(`pingdotgg/t3code`), not the fork's. The fork adds no models of its own, so pointing at the
source of the catalog keeps legacy classification current without a fork release.
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.34",
"version": "0.0.35",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
Loading
Loading