Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
91b5b88
fix: drop orphan gitlink .repos/alchemy-effect/.vendor/alchemy
pranav100000 Aug 8, 2026
d945b18
ci: run fork CI on GitHub-hosted runners; guard upstream-only deploy/…
pranav100000 Aug 8, 2026
8c7b79a
feat(aether): provider driver skeleton — settings, registration, vend…
pranav100000 Aug 8, 2026
824bc87
feat(aether): REST task client + adapter session core (#3)
pranav100000 Aug 8, 2026
0d4cf0f
feat(aether): workspace attach, WS transport, and the full event mapp…
pranav100000 Aug 8, 2026
c652e67
feat(aether): turn lifecycle, mirror sync engine, write guards — Aeth…
pranav100000 Aug 8, 2026
3d5217b
feat(aether): question/plan responses, revert ledger, model switch, p…
pranav100000 Aug 8, 2026
6913385
fix(aether): durable-authoritative turn settlement so checkpoint diff…
pranav100000 Aug 8, 2026
e4dcd25
fix(aether): skip clean-tree preflight for driver-owned worktrees (#10)
pranav100000 Aug 9, 2026
5a35c28
feat(aether): default new Aether threads to a fresh worktree in the c…
pranav100000 Aug 9, 2026
89925de
fix(aether): base managed-worktree cloud tasks on the fork branch, no…
pranav100000 Aug 9, 2026
9a28299
feat(aether): cloud port previews in the composer (#13)
pranav100000 Aug 9, 2026
2a06dc4
feat(aether): open cloud port preview in the in-app embedded browser …
pranav100000 Aug 9, 2026
26b340c
feat(aether): cloud terminal — attach a shell in the workspace VM (#15)
pranav100000 Aug 9, 2026
27b3f94
feat(aether): reconnect the cloud terminal on transient socket drops …
pranav100000 Aug 9, 2026
b76766b
feat(aether): surface cloud terminals in the cross-thread terminal li…
pranav100000 Aug 9, 2026
9536744
Merge upstream/main into aether-cloud-provider
pranav100000 Aug 11, 2026
4427f33
fix(aether): resolve Effect service-convention findings
pranav100000 Aug 11, 2026
8155f4c
fix(aether): stable structural detail on rest-client errors
pranav100000 Aug 11, 2026
3ae2e17
fix(aether): structural failure literal for the mirror read-only refusal
pranav100000 Aug 11, 2026
2bb0a73
fix(aether): stable detail on cloud-terminal errors; restore legacy d…
pranav100000 Aug 11, 2026
3732569
fix(aether): symlink-safe mirror writes, ordered binary apply, quoted…
pranav100000 Aug 11, 2026
d80beb3
fix(aether): canonicalize mirror-registry paths through symlinks
pranav100000 Aug 11, 2026
4880a58
fix(aether): loud typed failures and fast detach across the cloud tra…
pranav100000 Aug 11, 2026
e166c7e
fix(aether): settle discarded queued turns even when the stop is unco…
pranav100000 Aug 11, 2026
a41b33e
fix(server): no-op a stale managed-worktree attach instead of clobbering
pranav100000 Aug 11, 2026
cb413ef
fix: preserve an explicit start-from-origin toggle; keep the stub bra…
pranav100000 Aug 11, 2026
9ba2a2a
fix(server): tear down the bootstrap worktree when its attach is supe…
pranav100000 Aug 11, 2026
326dafa
fix(aether): honor interruptTurn's turn id; guard an unconfirmed late…
pranav100000 Aug 11, 2026
24bcd61
fix(aether): terminal routing/cleanup, backoff ladder, per-task mirro…
pranav100000 Aug 11, 2026
1378420
fix(aether): complete the at-most-once send pin; never settle Stop on…
pranav100000 Aug 11, 2026
483caf7
fix(aether): one deadline per REST exchange; serialize terminal open …
pranav100000 Aug 11, 2026
09bcb95
fix(mobile): default an Aether draft to an isolated worktree
pranav100000 Aug 11, 2026
9172d7f
fix(aether): pin a send only when its dispatch actually begins
pranav100000 Aug 12, 2026
10716ff
fix(server): close cloud terminals from the thread-deletion reactor
pranav100000 Aug 12, 2026
9bfa5c9
fix(mobile): keep the Aether worktree default provisional, not sticky
pranav100000 Aug 12, 2026
71fd731
fix(mobile): round-trip modeUserSet and queue with the effective mode
pranav100000 Aug 12, 2026
d8f9688
fix(aether): close the mirror-guard race between its check and its mu…
pranav100000 Aug 12, 2026
5747a1e
fix(server): close a cloud thread's stranded local terminals
pranav100000 Aug 12, 2026
a0ef509
fix(aether): put git.runStackedAction under the mirror claim lock too
pranav100000 Aug 12, 2026
713f2cd
fix(aether): scope the mirror claim lock to the paths a mutation writes
pranav100000 Aug 12, 2026
64ff102
fix(aether): freeze ALL claims for the removeWorktree guard
pranav100000 Aug 12, 2026
511c5d9
fix(aether): give the snapshot-build error a stable structural detail…
pranav100000 Aug 12, 2026
326efdc
Merge remote-tracking branch 'upstream/main' into aether-green
pranav100000 Aug 12, 2026
872dc5f
fix(mobile): follow upstream's renamed pending-input draft answer field
pranav100000 Aug 12, 2026
82149c7
style: wrap a long assertion in project.test.ts (biome)
pranav100000 Aug 12, 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
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,8 @@ concurrency:
jobs:
check:
name: Check
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
Expand DownExpand Up@@ -59,8 +59,8 @@ jobs:

test:
name: Test
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
Expand DownExpand Up@@ -112,8 +112,8 @@ jobs:

mobile_native_static_analysis:
name: Mobile Native Static Analysis
runs-on: blacksmith-6vcpu-macos-26
timeout-minutes: 10
runs-on: macos-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All@@ -140,8 +140,8 @@ jobs:

release_smoke:
name: Release Smoke
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy-relay.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ concurrency:
jobs:
deploy_relay:
name: Deploy production relay
# Upstream-only: the relay and its Cloudflare/Clerk/APNs credentials live in
# pingdotgg. On the Aether-Runtime fork this job must never run.
if: github.repository == 'pingdotgg/t3code'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
environment:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,9 @@ permissions:
jobs:
check_changes:
name: Check for changes since last nightly
if: github.event_name == 'schedule'
# The schedule fires on the Aether-Runtime fork too; nightly releases are
# upstream-only, so skip there (manual workflow_dispatch still works).
if: github.event_name == 'schedule' && github.repository == 'pingdotgg/t3code'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
Expand Down
1 change: 0 additions & 1 deletion .repos/alchemy-effect/.vendor/alchemy
Submodule alchemy deleted from c9f5e5
9 changes: 9 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,6 +49,15 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "aether") {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path fill="#6e72d6" d="M12 2 22.5 21h-5.06L12 11.13 6.56 21H1.5L12 2Z" />
<Path fill="#6e72d6" d="m12 15.4 3.1 5.6H8.9l3.1-5.6Z" />
</Svg>
);
}

if (props.provider === "opencode") {
return (
<Svg width={size} height={size} viewBox="0 0 32 40" fill="none">
Expand Down
94 changes: 75 additions & 19 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
DEFAULT_PROVIDER_INTERACTION_MODE,
DEFAULT_RUNTIME_MODE,
MessageId,
ProviderDriverKind,
T3_PROJECT_FILE_NAME,
ThreadId,
} from "@t3tools/contracts";
Expand DownExpand Up@@ -75,6 +76,9 @@ import {
} from "../home/homeThreadList";
import { useMobileProjectGroupingSettings } from "../../state/project-grouping";

/** Mirrors the web composer's own declaration (ChatView).*/
const AETHER_DRIVER_KIND = ProviderDriverKind.make("aether");

type WorkspaceMode = "local" | "worktree";

const EMPTY_BRANCH_REFS: ReadonlyArray<VcsRef> = [];
Expand DownExpand Up@@ -370,20 +374,6 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
if (t3ProjectFileData === null || t3ProjectFileData.truncated) return null;
return parseT3ProjectFile(t3ProjectFileData.contents)?.defaultThreadEnvMode ?? null;
}, [t3ProjectFileData]);
const defaultWorkspaceMode: WorkspaceMode = resolveDefaultThreadEnvMode({
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFile: t3ProjectFileDefaultMode,
globalDefault: selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ?? "local",
});
// While unsettled the resolved default is provisional. Nothing may write
// it into the draft during that window (the auto-branch effect does), or
// the frozen interim value beats the t3.json default once it loads.
const defaultWorkspaceModeSettled = isDefaultThreadEnvModeSettled({
explicitMode: selectedProjectDraft.workspaceSelection?.mode,
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFilePending: t3ProjectFileQuery.isPending,
});
const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode;
const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null;
const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null;
// Keep the user's explicit choice separate from the resolved display value:
Expand DownExpand Up@@ -428,6 +418,44 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
const selectedModelKey = selectedModel
? `${selectedModel.instanceId}:${selectedModel.model}`
: null;
// Resolved AFTER the model selection on purpose: the Aether driver turns the
// thread's checkout into a one-way MIRROR of its cloud VM — every settle
// resets it and re-applies the cloud tree. Pointed at the SHARED project
// checkout that would discard the user's own work, so an untouched draft
// with an Aether model selected defaults to an isolated worktree, the same
// safety default the web composer applies. An explicit pick still wins:
// `workspaceSelection.mode` is read ahead of this default below.
const selectedProviderIsAether =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highthreads/new-task-flow-provider.tsx:428

When the server config is unavailable, selectedProviderIsAether returns false even if the stored model selection targets an Aether driver, so defaultWorkspaceMode resolves to local instead of worktree. This lets a task be queued against the shared checkout before the config arrives, and when the Aether driver later settles its one-way mirror it discards the user's local work. The fallback option synthesized by buildModelOptions sets providerDriver to the provider instance ID rather than the actual driver kind, so the AETHER_DRIVER_KIND comparison fails. Consider tracking the driver kind alongside the stored selection or preventing queuing until the driver can be reliably classified.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/new-task-flow-provider.tsx around line 428:
When the server config is unavailable, `selectedProviderIsAether` returns `false` even if the stored model selection targets an Aether driver, so `defaultWorkspaceMode` resolves to `local` instead of `worktree`. This lets a task be queued against the shared checkout before the config arrives, and when the Aether driver later settles its one-way mirror it discards the user's local work. The fallback option synthesized by `buildModelOptions` sets `providerDriver` to the provider instance ID rather than the actual driver kind, so the `AETHER_DRIVER_KIND` comparison fails. Consider tracking the driver kind alongside the stored selection or preventing queuing until the driver can be reliably classified.

modelOptions.find((option) => option.key === selectedModelKey)?.providerDriver ===
AETHER_DRIVER_KIND;
const defaultWorkspaceMode: WorkspaceMode = selectedProviderIsAether
? "worktree"
: resolveDefaultThreadEnvMode({
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFile: t3ProjectFileDefaultMode,
globalDefault: selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ?? "local",
});
// While unsettled the resolved default is provisional. Nothing may write
// it into the draft during that window (the auto-branch effect does), or
// the frozen interim value beats the t3.json default once it loads.
const defaultWorkspaceModeSettled = isDefaultThreadEnvModeSettled({
explicitMode: selectedProjectDraft.workspaceSelection?.mode,
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFilePending: t3ProjectFileQuery.isPending,
});
// Only an EXPLICIT pick pins the mode. Incidental writes (selectBranch,
// setStartFromOrigin) carry the resolved mode along so the queued-task
// snapshot stays right, which would otherwise freeze the Aether worktree
// default into the draft as if the user had chosen it — and keep worktree
// after switching to a provider that never wanted it. Absent flag = a draft
// from before this field, treated as user-set so an existing pick stands.
const storedWorkspaceSelection = selectedProjectDraft.workspaceSelection;
const workspaceModeUserSet =
storedWorkspaceSelection !== undefined && storedWorkspaceSelection.modeUserSet !== false;
const workspaceMode =
workspaceModeUserSet && storedWorkspaceSelection !== undefined
? storedWorkspaceSelection.mode
: defaultWorkspaceMode;

const selectedModelOption =
modelOptions.find(
Expand DownExpand Up@@ -599,6 +627,8 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
updateComposerDraftSettings(selectedProjectDraftKey, {
workspaceSelection: {
mode,
// The one write that means "the user chose this".
modeUserSet: true,
branch: selectedBranchName,
worktreePath: selectedWorktreePath,
...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}),
Expand All@@ -616,13 +646,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
updateComposerDraftSettings(selectedProjectDraftKey, {
workspaceSelection: {
mode: workspaceMode,
modeUserSet: workspaceModeUserSet,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Queue mobile tasks with the effective workspace mode

When a branch selection stores modeUserSet: false and the user subsequently changes providers—for example, selecting a branch under a local provider and then switching to Aether—the displayed workspaceMode changes to worktree, but buildPendingTaskMessage still prefers the stale workspaceSelection.mode. Fresh evidence is this newly added path that deliberately persists a provisional mode while the queue serializer ignores the marker. The task is consequently queued in local mode, bypassing the Aether isolation default and allowing its one-way mirror to claim the shared checkout; use the effective mode whenever modeUserSet is false.

Useful? React with 👍 / 👎.

branch: branch.name,
worktreePath: normalizeSelectedWorktreePath(selectedProject, branch),
...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}),
},
});
},
[draftStartFromOrigin, selectedProject, selectedProjectDraftKey, workspaceMode],
[
draftStartFromOrigin,
selectedProject,
selectedProjectDraftKey,
workspaceMode,
workspaceModeUserSet,
],
);

const setStartFromOrigin = useCallback(
Expand All@@ -633,13 +670,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
updateComposerDraftSettings(selectedProjectDraftKey, {
workspaceSelection: {
mode: workspaceMode,
modeUserSet: workspaceModeUserSet,
branch: selectedBranchName,
worktreePath: selectedWorktreePath,
startFromOrigin: value,
},
});
},
[selectedBranchName, selectedProjectDraftKey, selectedWorktreePath, workspaceMode],
[
selectedBranchName,
selectedProjectDraftKey,
selectedWorktreePath,
workspaceMode,
workspaceModeUserSet,
],
);

const refreshBranches = branchState.refresh;
Expand DownExpand Up@@ -709,7 +753,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
runtimeMode: message.runtimeMode,
interactionMode: message.interactionMode,
workspaceSelection: {
// The queued task's mode is a decision already made for it, so
// reopening it for editing keeps that mode pinned rather than
// re-deriving a default over the top of it.
mode: message.creation.workspaceMode,
modeUserSet: true,
branch: message.creation.branch,
worktreePath: message.creation.worktreePath,
startFromOrigin: message.creation.startFromOrigin ?? false,
Expand DownExpand Up@@ -744,9 +792,17 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
return null;
}
const workspaceSelection = draft.workspaceSelection;
// Fall back to the resolved mode (server default) so queued tasks drain
// with the same mode the composer displayed.
const mode = workspaceSelection?.mode ?? workspaceMode;
// The stored mode outranks the resolved one ONLY when the user picked
// it. A mode that is merely a carried-along default must not win here:
// selecting a branch under a local provider stores `local`, and
// switching to Aether afterwards has to queue the task in the isolated
// worktree its one-way mirror requires — the composer already displays
// that, and the queue has to agree or the mirror claims the shared
// checkout on drain.
const mode =
workspaceSelection === undefined || workspaceSelection.modeUserSet === false
? workspaceMode
: workspaceSelection.mode;
// When the selection is the stand-in built from the queued snapshot,
// persist the original (possibly absent) snapshot values — the
// stand-in's placeholder title/workspaceRoot must never be written back
Expand Down
27 changes: 22 additions & 5 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
import * as Haptics from "expo-haptics";
import { type AppSymbolName, SymbolView } from "../../components/AppSymbol";
import { LayoutAnimation, Pressable, ScrollView, useColorScheme, View } from "react-native";
import {
LayoutAnimation,
Linking,
Pressable,
ScrollView,
useColorScheme,
View,
} from "react-native";

import { AppText as Text } from "../../components/AppText";
import { scaledTypographyLineHeight } from "../../lib/appearancePreferences";
Expand DownExpand Up@@ -162,16 +169,22 @@ export function ThreadWorkLog(props: {
{...(isFreshRow(row.createdAt) ? { entering: FadeIn.duration(200) } : {})}
>
<Pressable
accessibilityRole={canExpand ? "button" : undefined}
accessibilityRole={row.portPreview ? "link" : canExpand ? "button" : undefined}
accessibilityLabel={displayText}
accessibilityHint={
canExpand
? "Double tap to show full details. Long press to copy."
: "Long press to copy."
row.portPreview
? "Double tap to open the preview. Long press to copy."
: canExpand
? "Double tap to show full details. Long press to copy."
: "Long press to copy."
}
accessibilityState={canExpand ? { expanded } : undefined}
hitSlop={4}
onPress={() => {
if (row.portPreview) {
void Linking.openURL(row.portPreview.url);
return;
}
if (canExpand) {
triggerDisclosureFeedback();
props.onToggleRow(row.id);
Expand DownExpand Up@@ -213,6 +226,10 @@ export function ThreadWorkLog(props: {
<Text className="pr-1 font-t3-medium text-3xs text-emerald-600 dark:text-emerald-400">
Copied
</Text>
) : row.portPreview ? (
<Text className="pr-1 font-t3-medium text-3xs text-blue-600 dark:text-blue-400">
Open preview ›
</Text>
) : null}
<View className="h-4 w-4 items-center justify-center">
{canExpand ? (
Expand Down
Loading
Loading