Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
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
1b1e5b9
fix(web): remove the square thread info panel shadow (#15069)
PixPMusic Oct 4, 2026
8e39994
fix(mobile): Android usage widget no longer sticks on "Loading widget…
tris203 Oct 4, 2026
a6aebbe
fix(web): size the model picker to its content (#15152)
saphid Oct 4, 2026
2a45557
test(server): replay checks a Claude subagent's thread takes its repo…
juliusmarminge Oct 4, 2026
dab26f5
fix(web): subagent finish notifications look like subagent cards (#15…
flamboh Oct 4, 2026
4543c73
fix(web): thread status dot has an accessible name (#14587)
ryanilano Oct 4, 2026
c5bc98d
fix(web): legacy sidebar options button has a label (#14602)
ryanilano Oct 4, 2026
2b8bb0c
fix(web): imported themes keep switches and focus rings visible (#14498)
flamboh Oct 4, 2026
7bc161f
fix(web): links to issues no longer strand the pull request viewer (#…
flamboh Oct 4, 2026
237d51e
fix(web): repo/task breadcrumb no longer bounces when the sidebar col…
flamboh Oct 4, 2026
f90b77d
fix(web): Pull request panel entry works for linked PRs (#15061)
flamboh Oct 4, 2026
95edeb7
fix(web): add context menu to draft threads in the sidebar (#10637)
flamboh Oct 4, 2026
9efb016
fix(web): keep sidebar branding and build pills from clipping at vary…
flamboh Oct 4, 2026
0c81120
fix(usage): model shares and order follow the selected metric (#11391)
flamboh Oct 4, 2026
1826fb5
feat(web): sweep sidebar buttons to settle, un-settle, and wake threa…
argofowl Oct 4, 2026
7379933
feat: retry a failed workspace preparation (#15326)
juliusmarminge Oct 4, 2026
7ffe3a8
chore: πŸ”€ merge upstream nightly v0.0.46-nightly.20261004.2644
chamaquita-d3[bot] 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
11 changes: 10 additions & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,16 @@ const config: ExpoConfig = {
minSdkVersion: 24,
// kotlinx-io uses Kotlin 2.3's return-value checker annotation, while
// SDK 58 builds with Kotlin 2.2. It has no runtime behavior.
extraProguardRules: "-dontwarn kotlin.MustUseReturnValues",
//
// WorkManager 2.9 keeps InputMerger classes but not their constructors,
// and R8 full mode no longer keeps a default constructor implicitly.
// Without it no work request can start, so the Glance session behind
// the widget never renders and it stays on "Loading widget". WorkManager
// 2.10 ships this rule itself; drop it once the resolved version gets there.
extraProguardRules: [
"-dontwarn kotlin.MustUseReturnValues",
"-keep class * extends androidx.work.InputMerger { <init>(); }",
].join("\n"),
},
ios: {
deploymentTarget: "18.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 @@ -44,13 +44,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 @@ -466,8 +468,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 @@ -520,8 +525,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 @@ -533,6 +543,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 @@ -547,11 +558,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 @@ -606,6 +622,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 @@ -628,6 +647,7 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeEnvironmentIds,
threadListV2Layout,
v2PendingTasks,
workingShelfExpanded,
],
);

Expand Down Expand Up @@ -662,6 +682,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 @@ -730,7 +760,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 @@ -782,8 +812,10 @@ export function HomeScreen(props: HomeScreenProps) {
titleRegenerationEnvironmentIds,
toggleSettledShelf,
toggleSnoozedShelf,
toggleWorkingShelf,
v2ProjectTitleByProjectKey,
props.searchQuery,
workingShelfEnabled,
],
);
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
Expand All @@ -800,6 +832,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 @@ -808,6 +842,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