Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6108ef3
fix(server): runs no longer get stuck (#15048)
t3dotgg Oct 3, 2026
5691412
fix(usage): Codex Fast and Ultrafast now cost what they bill (#15101)
t3dotgg Oct 3, 2026
a5b34b2
fix(clients): a dev server left running no longer says the thread is …
t3dotgg Oct 3, 2026
f68e24f
fix(web): a thread that left a shell running shows its unseen complet…
Mnigos Oct 3, 2026
263097a
fix(web): mod+enter starts a new thread in the background again (#15060)
t3dotgg Oct 3, 2026
e8545b2
feat(usage): show cost by token type, speed, and model detail (#15108)
t3dotgg Oct 3, 2026
18b2132
feat(server): agents can watch a PR and get woken when checks, review…
t3dotgg Oct 3, 2026
31a9da1
fix(server): keep delegated review rounds on the task API (#15115)
t3dotgg Oct 3, 2026
f391794
fix(shared): classify workspace previews by literal filenames (#10311)
yashranaway Oct 3, 2026
1945ce8
fix(mobile): iOS threads no longer jump to the top (#14808)
znoraka Oct 3, 2026
44b4965
fix(web): reduce the gap above the draft composer (#15196)
Bil0000 Oct 3, 2026
a0bf1ee
fix(mobile): a dev server left running no longer shows the waiting bo…
jakeleventhal Oct 3, 2026
ce90eec
fix(server): a Claude command you stop shows as interrupted (#14896)
SunkenInTime Oct 3, 2026
0080e80
fix(server): editors appear once a slow discovery scan finishes (#13917)
bfowler Oct 3, 2026
aad7329
fix(server): Claude threads no longer stay stuck in plan mode Claude …
juliusmarminge Oct 3, 2026
65731f9
fix(mobile): show complete subagent details (#15189)
jakeleventhal Oct 3, 2026
77823bd
fix(mobile): an expired Live Activity no longer leaves a second card …
juliusmarminge Oct 3, 2026
cfdff56
test(web): remove redundant thread sort fallback tests (#15095)
t3-code[bot] Oct 3, 2026
70e5a40
fix(web): thinking row after a failed tool expands the run's tool cal…
maria-rcks Oct 3, 2026
39efcd8
perf(web): DOM changes no longer restyle the whole page (#15265)
flamboh Oct 3, 2026
71dbaf1
perf(usage): cut warm usage scans from seconds to milliseconds on lar…
t3dotgg Oct 3, 2026
fffe6e6
perf(web): virtualize command palette results (#15266)
flamboh Oct 3, 2026
0cfa113
chore(lint): flag :has() variants that restyle the whole page (#15274)
flamboh Oct 3, 2026
429c625
fix(web): workspace card docks beside chat when the window is narrow …
t3dotgg Oct 3, 2026
5e35272
feat(web): render mermaid code blocks as diagrams (#15067)
maria-rcks Oct 3, 2026
f1dcd93
feat(web): Nightly tells you to get the beta mobile app (#15070)
t3dotgg Oct 3, 2026
a21702d
test(server): ACP adapter tests no longer race the prompt settle (#15…
t3dotgg Oct 3, 2026
b05adb7
feat(usage): fold preview model IDs into the model they belong to (#1…
t3dotgg Oct 3, 2026
a78760e
refactor(server): check RPC scopes in group middleware (#15324)
juliusmarminge Oct 3, 2026
8d84666
feat(mobile): beta Working section hides busy threads until they need…
t3dotgg Oct 3, 2026
00eb8f6
fix(settings): symlinked settings files stay linked when saved (#15009)
yordis Oct 3, 2026
d65cd33
fix(server): Stop ends a dev server left running before a provider sw…
t3dotgg Oct 3, 2026
243d1e7
fix(server): merged threads settle even after the agent wakes on its …
t3dotgg Oct 3, 2026
c47f426
fix(web): no-project drafts can switch machines (#15356)
maria-rcks Oct 3, 2026
f786ff3
fix(web): highlight tool inputs and remove nested work log indentatio…
Yash-Singh1 Oct 3, 2026
5108c97
fix(server): restarts keep delegated tasks, queued threads, and stops…
maria-rcks Oct 3, 2026
a976f8c
fix(web): sending past the resume banner compacts first (#15290)
maria-rcks Oct 3, 2026
44bd4c9
fix(codex): resume archived native sessions (#15389)
maria-rcks Oct 3, 2026
f2cc80a
feat(web): morph composer and panel action icons (#14924)
jakeleventhal Oct 4, 2026
d307127
fix(web): subagents sent a follow-up show as running in Lineage (#15334)
scratchyone Oct 4, 2026
2d81e4c
fix(web): clear stale chat action shortcuts (#15394)
maria-rcks Oct 4, 2026
88744f3
fix(orchestration-v2): restore earlier app agent transcript pages (#1…
Bil0000 Oct 4, 2026
df43386
chore: merge upstream pingdotgg/t3code
andrewcai8 Oct 4, 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
28 changes: 28 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,34 @@ describe("DesktopSettings", () => {
),
);

it.effect("saves through a symlinked settings file without replacing the link", () =>
withSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
const dotfiles = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-settings-dotfiles-",
});
const linkedSettingsPath = `${dotfiles}/desktop-settings.json`;
yield* fileSystem.writeFileString(linkedSettingsPath, "{}\n");
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.symlink(linkedSettingsPath, environment.desktopSettingsPath);

yield* settings.setServerExposureMode("network-accessible");

assert.equal(
yield* fileSystem.readLink(environment.desktopSettingsPath),
linkedSettingsPath,
);
const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(linkedSettingsPath),
);
assert.equal(persisted.serverExposureMode, "network-accessible");
}),
),
);

it.effect("migrates legacy implicit update channels to the runtime default", () =>
withSettings(
Effect.gen(function* () {
Expand Down
20 changes: 17 additions & 3 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type DesktopUpdateChannel,
} from "@t3tools/contracts";
import { fromLenientJson } from "@t3tools/shared/schemaJson";
import { resolveSymlinkTarget } from "@t3tools/shared/symlink";
import * as Context from "effect/Context";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -130,6 +131,7 @@ const settingsChange = (settings: DesktopSettings, changed: boolean): DesktopSet

const DesktopSettingsWriteOperation = Schema.Literals([
"create-temporary-file-name",
"resolve-symlink",
"encode-document",
"create-directory",
"write-temporary-file",
Expand Down Expand Up @@ -421,8 +423,20 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp
readonly defaultSettings: DesktopSettings;
readonly suffix: string;
}): Effect.fn.Return<void, DesktopSettingsWriteError> {
const directory = input.path.dirname(input.settingsPath);
const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
const targetPath = yield* resolveSymlinkTarget(input.settingsPath).pipe(
Effect.provideService(FileSystem.FileSystem, input.fileSystem),
Effect.provideService(Path.Path, input.path),
Effect.mapError(
(cause) =>
new DesktopSettingsWriteError({
operation: "resolve-symlink",
path: input.settingsPath,
cause,
}),
),
);
const directory = input.path.dirname(targetPath);
const tempPath = `${targetPath}.${process.pid}.${input.suffix}.tmp`;
const encoded = yield* encodeDesktopSettingsJson(
toDesktopSettingsDocument(input.settings, input.defaultSettings),
).pipe(
Expand Down Expand Up @@ -455,7 +469,7 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp
}),
),
);
yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe(
yield* input.fileSystem.rename(tempPath, targetPath).pipe(
Effect.mapError(
(cause) =>
new DesktopSettingsWriteError({
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ describe("DesktopClientSettings", () => {
),
);

it.effect("saves through a symlinked client settings file without replacing the link", () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
const dotfiles = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-client-settings-dotfiles-",
});
const linkedSettingsPath = `${dotfiles}/client-settings.json`;
yield* fileSystem.writeFileString(linkedSettingsPath, "{}\n");
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.symlink(linkedSettingsPath, environment.clientSettingsPath);

yield* settings.set(clientSettings);

assert.equal(
yield* fileSystem.readLink(environment.clientSettingsPath),
linkedSettingsPath,
);
assert.deepEqual(
yield* decodeClientSettingsJson(yield* fileSystem.readFileString(linkedSettingsPath)),
clientSettings,
);
}),
),
);

it.effect.each([
{ label: "permission", reason: "PermissionDenied" },
{ label: "I/O", reason: "Unknown" },
Expand Down
20 changes: 17 additions & 3 deletions apps/desktop/src/settings/DesktopClientSettings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts";
import { fromLenientJson } from "@t3tools/shared/schemaJson";
import { resolveSymlinkTarget } from "@t3tools/shared/symlink";
import * as Context from "effect/Context";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -41,6 +42,7 @@ export class DesktopClientSettingsReadError extends Schema.TaggedError<DesktopCl

const DesktopClientSettingsWriteOperation = Schema.Literals([
"create-temporary-file-name",
"resolve-symlink",
"encode-document",
"create-directory",
"write-temporary-file",
Expand Down Expand Up @@ -126,8 +128,20 @@ const writeClientSettings = Effect.fnUntraced(function* (input: {
readonly settings: ClientSettings;
readonly suffix: string;
}): Effect.fn.Return<void, DesktopClientSettingsWriteError> {
const directory = input.path.dirname(input.settingsPath);
const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
const targetPath = yield* resolveSymlinkTarget(input.settingsPath).pipe(
Effect.provideService(FileSystem.FileSystem, input.fileSystem),
Effect.provideService(Path.Path, input.path),
Effect.mapError(
(cause) =>
new DesktopClientSettingsWriteError({
operation: "resolve-symlink",
path: input.settingsPath,
cause,
}),
),
);
const directory = input.path.dirname(targetPath);
const tempPath = `${targetPath}.${process.pid}.${input.suffix}.tmp`;
const encoded = yield* encodeClientSettingsJson(input.settings).pipe(
Effect.mapError(
(cause) =>
Expand Down Expand Up @@ -158,7 +172,7 @@ const writeClientSettings = Effect.fnUntraced(function* (input: {
}),
),
);
yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe(
yield* input.fileSystem.rename(tempPath, targetPath).pipe(
Effect.mapError(
(cause) =>
new DesktopClientSettingsWriteError({
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"react-native": "0.88.0-rc.3",
"react-native-gesture-handler": "~3.2.1",
"react-native-image-viewing": "^0.2.2",
"react-native-keyboard-controller": "1.22.4",
"react-native-keyboard-controller": "1.22.6",
"react-native-nitro-markdown": "^0.5.0",
"react-native-nitro-modules": "0.35.9",
"react-native-reanimated": "4.7.0",
Expand Down
39 changes: 37 additions & 2 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ import {
ThreadListV2SettledShelfHeader,
ThreadListV2ShowMoreRow,
ThreadListV2SnoozedShelfHeader,
ThreadListV2WorkingShelfHeader,
} from "../threads/thread-list-v2-items";
import { useThreadRowProviderInstanceResolver } from "../threads/thread-provider-instance";
import {
buildThreadListV2Items,
getThreadListV2OrderedSection,
buildThreadListV2ListItems,
threadListV2ListItemsAreEqual,
threadListInboxReturns,
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
type ThreadListV2ListItem,
Expand Down Expand Up @@ -467,8 +469,11 @@ export function HomeScreen(props: HomeScreenProps) {
loaded: shelfPreferencesLoaded,
settledShelfExpanded,
snoozedShelfExpanded,
workingShelfEnabled,
workingShelfExpanded,
toggleSettledShelf,
toggleSnoozedShelf,
toggleWorkingShelf,
} = useThreadListV2ShelfPreferences();
// The queued-start and snooze helpers need a clock while the list stays open.
const [nowMinute, setNowMinute] = useState(() => new Date().toISOString().slice(0, 16));
Expand Down Expand Up @@ -524,8 +529,13 @@ export function HomeScreen(props: HomeScreenProps) {
queuedThreadKeys,
}),
});
return new Map([...sectionAvailability("pinned"), ...sectionAvailability("active")]);
// The Working beta orders the inbox by time, so only pins can move.
return new Map([
...sectionAvailability("pinned"),
...(workingShelfEnabled ? [] : sectionAvailability("active")),
]);
}, [
workingShelfEnabled,
pinReorderEnvironmentIds,
activeReorderEnvironmentIds,
props.threads,
Expand All @@ -537,6 +547,7 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeWakeTick,
]);
const threadListV2Layout = useMemo(() => {
threadListInboxReturns.observe(workingShelfEnabled ? props.threads : null);
// Settled threads are live shells; archived threads keep their original
// "hidden from lists" meaning.
return buildThreadListV2Items({
Expand All @@ -551,11 +562,16 @@ export function HomeScreen(props: HomeScreenProps) {
queuedThreadKeys,
settledLimit: settledVisibleCount,
now: new Date().toISOString(),
workingShelfEnabled,
workingShelfExpanded,
inboxReturnAt: threadListInboxReturns.returnedAt,
snoozedShelfExpanded,
settledShelfExpanded,
selectedThreadKey: null,
});
}, [
workingShelfEnabled,
workingShelfExpanded,
pendingOrder,
queuedThreadKeys,
nowMinute,
Expand Down Expand Up @@ -610,6 +626,9 @@ export function HomeScreen(props: HomeScreenProps) {
buildThreadListV2ListItems({
items: threadListV2Layout.items,
pendingTasks: v2PendingTasks,
workingCount: threadListV2Layout.workingCount,
workingShelfExpanded,
workingShelfHeaderIndex: threadListV2Layout.workingShelfHeaderIndex,
snoozedCount: threadListV2Layout.snoozedCount,
snoozedShelfExpanded,
snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
Expand All @@ -632,6 +651,7 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeEnvironmentIds,
threadListV2Layout,
v2PendingTasks,
workingShelfExpanded,
],
);

Expand Down Expand Up @@ -666,6 +686,16 @@ export function HomeScreen(props: HomeScreenProps) {
/>
);
}
if (item.type === "v2-working-shelf") {
return (
<ThreadListV2WorkingShelfHeader
count={item.count}
disabled={item.disabled}
expanded={item.expanded}
onToggle={toggleWorkingShelf}
/>
);
}
if (item.type === "v2-snoozed-shelf") {
return (
<ThreadListV2SnoozedShelfHeader
Expand Down Expand Up @@ -734,7 +764,7 @@ export function HomeScreen(props: HomeScreenProps) {
reorderSupported={
item.item.pinned
? pinReorderEnvironmentIds.has(thread.environmentId)
: activeReorderEnvironmentIds.has(thread.environmentId)
: !workingShelfEnabled && activeReorderEnvironmentIds.has(thread.environmentId)
}
canMoveUp={item.canMoveUp}
canMoveDown={item.canMoveDown}
Expand Down Expand Up @@ -786,8 +816,10 @@ export function HomeScreen(props: HomeScreenProps) {
titleRegenerationEnvironmentIds,
toggleSettledShelf,
toggleSnoozedShelf,
toggleWorkingShelf,
v2ProjectTitleByProjectKey,
props.searchQuery,
workingShelfEnabled,
],
);
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
Expand All @@ -804,6 +836,8 @@ export function HomeScreen(props: HomeScreenProps) {
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
threadSearchMatchByKey,
// Rows read it for their reorder menu items.
workingShelfEnabled,
}),
[
projectByKey,
Expand All @@ -812,6 +846,7 @@ export function HomeScreen(props: HomeScreenProps) {
listEnvironments,
threadSearchMatchByKey,
v2ProjectTitleByProjectKey,
workingShelfEnabled,
],
);

Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/features/settings/SettingsThreadsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function SettingsThreadsRouteScreen() {
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }}
>
<AutoSettleSettingsRows />
<BetaSettingsSection />
<LegacySettingsSection />
</ScrollView>
</SettingsScreen>
Expand Down Expand Up @@ -239,6 +240,35 @@ function AutoSettleSettingsRows() {
);
}

/**
* Device-local beta toggles, the counterpart of web's Working section (beta)
* in Settings → General.
*/
function BetaSettingsSection() {
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
const preferences = useAtomValue(mobilePreferencesAtom);
const workingShelfEnabled =
AsyncResult.isSuccess(preferences) && preferences.value.workingShelfEnabled === true;

return (
<View className="gap-3">
<SettingsSection title="Beta">
<SettingsSwitchRow
icon="bolt.circle"
label="Working section"
value={workingShelfEnabled}
onValueChange={(value) => savePreferences({ workingShelfEnabled: value })}
/>
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Fold working and monitoring threads into a Working section. They return to the top of the
list when they need you. While this is on, active threads are ordered by time and cannot be
moved.
</Text>
</View>
);
}

/**
* Device-local legacy toggles. Mobile has no client-settings sync, so this is
* the counterpart of web's Settings → General → Legacy features backed by
Expand Down
Loading
Loading