Skip to content

Commit dc3429b

Browse files
authored
Merge pull request #15 from pylon-code/upstream/2026-08-14-web
feat: adopt auto-settle-on-merge, wake icon alignment, and stage artwork
2 parents bcefea2 + 4dbf337 commit dc3429b

20 files changed

Lines changed: 329 additions & 77 deletions

.agents/upstream-review.md

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
remote: t3code-upstream
33
branch: main
4-
reviewed-through: "bad1143b02f7b585d1fe1335b3d9a97983ce8d8b"
5-
reviewed-through-date: "2026-08-13"
4+
reviewed-through: "5304f3e9d4c912bfa0eb2f5f41fa109b3646236b"
5+
reviewed-through-date: "2026-08-14"
66
---
77

88
# T3 upstream review log
@@ -945,12 +945,77 @@ Two tooling notes worth carrying forward:
945945
markdown tests ran against pre-cherry-pick source and failed against the
946946
updated expectations. **Re-run `vp i` after changing one of these modules.**
947947

948+
## 2026-08-14 — `bad1143b02f7b585d1fe1335b3d9a97983ce8d8b..5304f3e9d4c912bfa0eb2f5f41fa109b3646236b`
949+
950+
Six commits, six change sets, **four adopted and two skipped**. Nothing
951+
deferred, so the register stays empty. `git cherry` reported all six as `+`.
952+
953+
Conflict risk was measured by dry cherry-pick against the merged `pylon` before
954+
the brief was written: four clean, two conflicting — and both conflicts landed
955+
exactly on Pylon-owned boundaries, which is the system working.
956+
957+
Adopted across two branches, split by surface rather than by size:
958+
`upstream/2026-08-14-web` (PR #15) and `upstream/2026-08-14-mobile` (PR #16).
959+
960+
| Change set | Upstream | Decision | Pylon reference | Rationale or revisit condition |
961+
| ---------- | --------------------- | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
962+
| N1 | `db1507e98` / `#5880` | adopted | `07a3a846c` | A setting to stop threads auto-settling when their pull request merges. Additive `sidebarAutoSettleOnMerge`, **default `true`**, so existing behaviour is unchanged; closed pull requests still always settle. Upstream covered contracts, client-runtime, web, and mobile. |
963+
| N2 | `96bfa67b3` / `#6215` | adopted | `fb284b0f9` | Snoozed-thread wake icon sits on the row's optical centre. Generic geometry, applies to Pylon's sidebar unchanged. |
964+
| N3 | `23d45d914` / `#6535` | adopted | `570b54268`, `f7311ad81` | Stage artwork palette rework: explicit night pigments plus glow and sparkle, with the old `color-mix` derivation scoped to `t3-chat`, `ocean`, and `iris` — all of which Pylon still ships. Its "T3 Code artwork palettes" comment was rebranded, following `O11`. |
965+
| N5 | `85389b988` / `#6224` | adopted | `bbd3db454`, `5990b2440` | Mobile task and thread settings nest in bottom sheets. 41 files, +3,652/−1,836, **including native patches** to `react-native-screens` and `@react-navigation/native-stack`. Verified by a real prebuild, `pod install`, native build, and simulator pass — see below. |
966+
| N4 | `5ff3a03ad` / `#6086` | skipped | `` | Adds `-translate-y-px` to the sidebar brand label. A 1px optical correction measured against `<T3Wordmark />`; Pylon renders `<PylonMark />` plus the word "Pylon", a different glyph. **Cherry-picks clean and is still wrong for the fork.** Revisit only as Pylon-owned alignment work against Pylon's own mark. |
967+
| N6 | `5304f3e9d` | skipped | `` | Bumps the mobile app version to `1.0.4`. Pylon's `app.config.ts` is independent (`slug: "pylon"`, `pylon-code*` schemes, version `1.0.1`), so the bump conflicts and carries no Pylon meaning. Same class as any T3 release chore. |
968+
969+
**`N5` corrected a stale fact this ledger itself recorded.** The ninth batch's
970+
mobile notes said the `t3code-dev://` URL scheme "is still compatibility-named
971+
and works". It does not. A built `PylonDev.app` registers exactly
972+
`pylon-code-dev` and `com.pylon.code.dev`:
973+
974+
```
975+
$ plutil -extract CFBundleURLTypes json -o - .../PylonDev.app/Info.plist
976+
[{"CFBundleURLSchemes":["pylon-code-dev","com.pylon.code.dev"]},{"CFBundleURLSchemes":["exp+pylon"]}]
977+
```
978+
979+
`apps/mobile/src/App.tsx` still lists `t3code-dev://` among React Navigation's
980+
linking prefixes, which is likely how the belief survived, but iOS never
981+
delivers an unregistered scheme so the prefix is unreachable. Upstream's new
982+
`pair-client.sh` defaulted to that scheme and hardcoded `com.t3tools.t3code.dev`,
983+
so it would have failed silently on Pylon. The helper, the `test-pylon-mobile`
984+
skill's identity block, and its `T3CodeDev.xcworkspace` references were all
985+
corrected to what `app.config.ts` produces: `Pylon Dev`, `com.pylon.code.dev`,
986+
`pylon-code-dev`, `PylonDev.xcworkspace`, scheme `PylonDev`.
987+
988+
Verification: 264 tests over four files and five clean package typechecks on the
989+
web branch; 40 tests over six files and a clean mobile typecheck on the mobile
990+
branch; `vp lint` clean on both. `N5` additionally got a full native rebuild and
991+
an iOS Simulator pass — build succeeded in 176s with the new patches, the app
992+
launched as `com.pylon.code.dev`, the corrected deep link routed, and both the
993+
Add Environment route and the new "Choose project" context picker rendered as
994+
nested bottom sheets. `N3` was captured before and after on one dev server by
995+
swapping `index.css` over HMR, so the comparison holds data and layout constant.
996+
997+
**Android was not exercised for `N5`**, and the thread-settings sheet was not
998+
driven on a live thread because the simulator was reconnecting to a
999+
previously-paired real environment.
1000+
1001+
Two environment notes worth carrying forward:
1002+
1003+
- **CocoaPods 1.17.0 crashes under Ruby 4.0.6** with `Unicode Normalization not
1004+
appropriate for ASCII-8BIT` when `LANG`/`LC_ALL` are unset, which is how
1005+
`expo prebuild` leaves the shell. The ninth batch predicted this; setting a
1006+
UTF-8 locale for `pod install` fixes it. `expo prebuild` **exits 0 even when
1007+
its CocoaPods step fails**, leaving `ios/` without an `.xcworkspace`.
1008+
- **`apps/mobile/package.json`'s `dev:client` script still passes
1009+
`--scheme t3code-dev`**, which no longer matches the registered native scheme.
1010+
Left alone as out of scope for an adoption batch, but it is a real bug worth
1011+
its own fix.
1012+
9481013
## Deferred register
9491014

9501015
_The register is currently empty. DEF-1 and DEF-2 were adopted on 2026-08-11
951-
(see the sixth batch above); the 2026-08-12 eighth batch and the 2026-08-13
952-
ninth batch each deferred nothing new. Entries are removed once adopted or
953-
skipped, so an empty register means nothing is waiting._
1016+
(see the sixth batch above); the eighth, ninth, and 2026-08-14 tenth batches
1017+
each deferred nothing new. Entries are removed once adopted or skipped, so an
1018+
empty register means nothing is waiting._
9541019

9551020
Upstream work that has been reviewed and consciously _not_ adopted yet, with
9561021
the condition that should trigger a fresh look. Entries stay here until they

apps/desktop/src/settings/DesktopClientSettings.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const clientSettings: ClientSettings = {
3232
providerModelPreferences: {},
3333
showProviderUsageInContextPopover: true,
3434
sidebarAutoSettleAfterDays: 3,
35+
sidebarAutoSettleOnMerge: true,
3536
sidebarProjectGroupingMode: "repository_path",
3637
sidebarProjectGroupingOverrides: {
3738
"environment-1:/tmp/project-a": "separate",

apps/mobile/src/features/home/HomeScreen.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ export function HomeScreen(props: HomeScreenProps) {
207207
>(() => new Map());
208208
const preferencesResult = useAtomValue(mobilePreferencesAtom);
209209
const threadListV2Enabled = useThreadListV2Enabled();
210+
const autoSettleOnMerge =
211+
!AsyncResult.isSuccess(preferencesResult) ||
212+
preferencesResult.value.autoSettleOnMerge !== false;
210213
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
211214
const openSwipeableRef = useRef<SwipeableMethods | null>(null);
212215
const listRef = useRef<LegendListRef | null>(null);
@@ -483,8 +486,8 @@ export function HomeScreen(props: HomeScreenProps) {
483486
// Settled threads stay in the live shell stream (settled ≠ archived), so
484487
// the partition works directly off live shells — no snapshot merging or
485488
// optimistic holds.
486-
// PR states stream in per-row (rows own the VCS subscriptions); a merged or
487-
// closed PR auto-settles its thread on the next partition (mirrors web).
489+
// PR states stream in per-row. The next partition applies the configured
490+
// merge rule and the always-on close rule, matching web.
488491
const [changeRequestStateByKey, setChangeRequestStateByKey] = useState<
489492
ReadonlyMap<string, "open" | "closed" | "merged">
490493
>(() => new Map());
@@ -665,6 +668,7 @@ export function HomeScreen(props: HomeScreenProps) {
665668
searchQuery: props.searchQuery,
666669
matchedThreadKeys,
667670
changeRequestStateByKey,
671+
autoSettleOnMerge,
668672
settlementEnvironmentIds,
669673
snoozeEnvironmentIds,
670674
settledLimit: settledVisibleCount,
@@ -676,6 +680,7 @@ export function HomeScreen(props: HomeScreenProps) {
676680
});
677681
}, [
678682
changeRequestStateByKey,
683+
autoSettleOnMerge,
679684
nowMinute,
680685
snoozeWakeTick,
681686
snoozedShelfExpanded,

apps/mobile/src/features/settings/SettingsRouteScreen.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,21 @@ function ConfiguredSettingsRouteScreen() {
522522
}
523523

524524
function GeneralSettingsSection() {
525+
const preferencesResult = useAtomValue(mobilePreferencesAtom);
526+
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
527+
const autoSettleOnMerge =
528+
!AsyncResult.isSuccess(preferencesResult) ||
529+
preferencesResult.value.autoSettleOnMerge !== false;
530+
525531
return (
526532
<SettingsSection title="General">
527533
<SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" />
534+
<SettingsSwitchRow
535+
icon="arrow.triangle.branch"
536+
label="Auto-settle merged threads"
537+
value={autoSettleOnMerge}
538+
onValueChange={(value) => savePreferences({ autoSettleOnMerge: value })}
539+
/>
528540
<SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />
529541
</SettingsSection>
530542
);

apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { LegendList } from "@legendapp/list/react-native";
1111
import type { MenuAction } from "@react-native-menu/menu";
1212
import { useAtomValue } from "@effect/atom-react";
13+
import { AsyncResult } from "effect/unstable/reactivity";
1314
import type { EnvironmentId } from "@t3tools/contracts";
1415
import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
1516
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
@@ -29,6 +30,7 @@ import { NativeStackScreenOptions } from "../../native/StackHeader";
2930
import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
3031
import { useThemeColor } from "../../lib/useThemeColor";
3132
import { useProjects, useThreadShells } from "../../state/entities";
33+
import { mobilePreferencesAtom } from "../../state/preferences";
3234
import { useThreadSearch } from "../../state/queries";
3335
import { useThreadListV2Enabled } from "./use-thread-list-v2-enabled";
3436
import { environmentServerConfigsAtom } from "../../state/server";
@@ -214,6 +216,10 @@ function ThreadNavigationSidebarPane(
214216
regenerateThreadTitle,
215217
} = useThreadListActions();
216218
const threadListV2Enabled = useThreadListV2Enabled();
219+
const preferencesResult = useAtomValue(mobilePreferencesAtom);
220+
const autoSettleOnMerge =
221+
!AsyncResult.isSuccess(preferencesResult) ||
222+
preferencesResult.value.autoSettleOnMerge !== false;
217223
const pendingTasks = usePendingNewTasks();
218224
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
219225
const environments = useMemo(
@@ -411,8 +417,8 @@ function ThreadNavigationSidebarPane(
411417

412418
// Thread List v2 (beta) support — same model as the compact Home list
413419
// (HomeScreen.tsx): flat creation-order card block + settled recency tail.
414-
// PR states stream in per-row; merged/closed PRs auto-settle their thread
415-
// on the next partition.
420+
// PR states stream in per-row. The next partition applies the configured
421+
// merge rule and the always-on close rule.
416422
const [changeRequestStateByKey, setChangeRequestStateByKey] = useState<
417423
ReadonlyMap<string, "open" | "closed" | "merged">
418424
>(() => new Map());
@@ -546,6 +552,7 @@ function ThreadNavigationSidebarPane(
546552
searchQuery: props.searchQuery,
547553
matchedThreadKeys,
548554
changeRequestStateByKey,
555+
autoSettleOnMerge,
549556
settlementEnvironmentIds,
550557
snoozeEnvironmentIds,
551558
settledLimit: settledVisibleCount,
@@ -557,6 +564,7 @@ function ThreadNavigationSidebarPane(
557564
});
558565
}, [
559566
changeRequestStateByKey,
567+
autoSettleOnMerge,
560568
nowMinute,
561569
snoozeWakeTick,
562570
snoozedShelfExpanded,

apps/mobile/src/features/threads/thread-list-v2-items.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
369369
readonly canMovePinnedDown?: boolean;
370370
readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void;
371371
readonly onSwipeableClose: (methods: SwipeableMethods) => void;
372-
/** Reports this row's live PR state up so the partition can auto-settle
373-
merged/closed work (mirrors web's onChangeRequestState). */
372+
/** Reports this row's live PR state for the partition's merge and close
373+
rules. Mirrors web's onChangeRequestState. */
374374
readonly onChangeRequestState?: (
375375
threadKey: string,
376376
state: "open" | "closed" | "merged" | null,

apps/mobile/src/features/threads/threadListV2.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,21 @@ describe("sortThreadsForListV2", () => {
263263
});
264264

265265
describe("buildThreadListV2Items", () => {
266+
it("keeps a merged thread active when auto-settle on merge is off", () => {
267+
const merged = makeThread({ id: ThreadId.make("merged"), title: "Merged" });
268+
const layout = buildThreadListV2Items({
269+
threads: [merged],
270+
environmentId: null,
271+
searchQuery: "",
272+
changeRequestStateByKey: new Map([[`${environmentId}:${merged.id}`, "merged"]]),
273+
autoSettleOnMerge: false,
274+
now: NOW,
275+
});
276+
277+
expect(layout.items.map((item) => item.thread.id)).toEqual(["merged"]);
278+
expect(layout.settledCount).toBe(0);
279+
});
280+
266281
it("hides snoozed threads and counts them — visibility parity with web", () => {
267282
const layout = buildThreadListV2Items({
268283
threads: [

apps/mobile/src/features/threads/threadListV2.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,8 @@ export function buildThreadListV2ListItems(input: {
306306

307307
/**
308308
* Partitions visible threads into the active card block (creation order) and
309-
* the settled recency tail, matching the web v2 list. `autoSettleAfterDays`
310-
* mirrors the web default of 3 — mobile has no client-settings sync yet, so
311-
* the default is fixed here rather than user-configurable.
309+
* the settled recency tail, matching the web v2 list. Mobile stores these
310+
* auto-settle preferences per device.
312311
*/
313312
export function buildThreadListV2Items(input: {
314313
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
@@ -329,6 +328,7 @@ export function buildThreadListV2Items(input: {
329328
contract as settlementEnvironmentIds. */
330329
readonly snoozeEnvironmentIds?: ReadonlySet<EnvironmentId>;
331330
readonly autoSettleAfterDays?: number;
331+
readonly autoSettleOnMerge?: boolean;
332332
/** Max settled rows to render; the rest are counted, not built. */
333333
readonly settledLimit?: number;
334334
/** Injectable for tests; defaults to now. */
@@ -349,6 +349,7 @@ export function buildThreadListV2Items(input: {
349349
const now = input.now ?? new Date().toISOString();
350350
const snoozeNow = input.snoozeNow ?? now;
351351
const autoSettleAfterDays = input.autoSettleAfterDays ?? 3;
352+
const autoSettleOnMerge = input.autoSettleOnMerge ?? true;
352353
const query = input.searchQuery.trim().toLocaleLowerCase();
353354
const projectKeys = input.projectRefs
354355
? new Set(input.projectRefs.map((ref) => `${ref.environmentId}:${ref.projectId}`))
@@ -405,7 +406,12 @@ export function buildThreadListV2Items(input: {
405406
}
406407
if (
407408
supportsSettlement &&
408-
effectiveSettled(thread, { now, autoSettleAfterDays, changeRequestState })
409+
effectiveSettled(thread, {
410+
now,
411+
autoSettleAfterDays,
412+
autoSettleOnMerge,
413+
changeRequestState,
414+
})
409415
) {
410416
settled.push(thread);
411417
} else {

apps/mobile/src/persistence/mobile-preferences.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface Preferences {
2626
/** @deprecated Kept temporarily so older OTA bundles retain the selected mode. */
2727
readonly projectGroupingEnabled?: boolean;
2828
readonly projectGroupingMode?: SidebarProjectGroupingMode;
29+
readonly autoSettleOnMerge?: boolean;
2930
/**
3031
* Device-local mirror of the web `legacySidebarEnabled` setting. Mobile has
3132
* no client-settings sync, so the legacy grouped thread list is opted into
@@ -85,6 +86,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
8586
collapsedProjectGroups?: readonly string[];
8687
projectGroupingEnabled?: boolean;
8788
projectGroupingMode?: SidebarProjectGroupingMode;
89+
autoSettleOnMerge?: boolean;
8890
legacyThreadListEnabled?: boolean;
8991
} = {};
9092

@@ -122,6 +124,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
122124
) {
123125
preferences.projectGroupingMode = parsed.projectGroupingMode;
124126
}
127+
if (typeof parsed.autoSettleOnMerge === "boolean") {
128+
preferences.autoSettleOnMerge = parsed.autoSettleOnMerge;
129+
}
125130
if (typeof parsed.legacyThreadListEnabled === "boolean") {
126131
preferences.legacyThreadListEnabled = parsed.legacyThreadListEnabled;
127132
}

apps/web/src/components/ChatView.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type EnvironmentConnectionPresentation,
2828
} from "@t3tools/client-runtime/connection";
2929
import {
30+
changeRequestAutoSettles,
3031
effectiveSettled,
3132
effectiveSnoozed,
3233
threadWokeAt,
@@ -4105,6 +4106,7 @@ function ChatViewContent(props: ChatViewProps) {
41054106
// so the banner and the sidebar row never disagree.
41064107
const activeThreadShell = useThreadShell(isServerThread ? activeThreadRef : null);
41074108
const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays);
4109+
const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge);
41084110
const activeThreadPr = resolveThreadPr({
41094111
threadBranch: activeThread?.branch ?? null,
41104112
gitStatus: gitStatusQuery.data ?? null,
@@ -4145,15 +4147,14 @@ function ChatViewContent(props: ChatViewProps) {
41454147
if (activeThreadRef === null || activeThreadWokeAt === null) return;
41464148
markThreadVisited(scopedThreadKey(activeThreadRef), activeThreadWokeAt);
41474149
}, [activeThreadRef, activeThreadWokeAt, markThreadVisited]);
4148-
// Mirror of the sidebar's Woke pill for the open thread: same visit
4149-
// comparison, same merged/closed-PR suppression (finished work needs no
4150-
// wake-up call). Drives the dismissible composer banner below.
4150+
// Mirror of the sidebar's Woke pill for the open thread. It uses the same
4151+
// visit comparison and change request settle rule.
41514152
const activeThreadLastVisitedAt = useUiStateStore((store) =>
41524153
activeThreadKey === null ? undefined : store.threadLastVisitedAtById[activeThreadKey],
41534154
);
41544155
const activeThreadWokeVisible = useMemo(() => {
41554156
if (activeThreadWokeAt === null) return false;
4156-
if (activeThreadPr?.state === "merged" || activeThreadPr?.state === "closed") return false;
4157+
if (changeRequestAutoSettles(activeThreadPr?.state, autoSettleOnMerge)) return false;
41574158
const wokeAtMs = Date.parse(activeThreadWokeAt);
41584159
if (Number.isNaN(wokeAtMs)) return false;
41594160
// Having the thread open counts as a visit at completedAt (the effect
@@ -4175,18 +4176,21 @@ function ChatViewContent(props: ChatViewProps) {
41754176
activeThreadLastVisitedAt,
41764177
activeThreadPr?.state,
41774178
activeThreadWokeAt,
4179+
autoSettleOnMerge,
41784180
]);
41794181
const activeThreadSettled = useMemo(() => {
41804182
if (activeThreadShell === null || !supportsSettlement) return false;
41814183
return effectiveSettled(activeThreadShell, {
41824184
now: `${nowMinute}:00.000Z`,
41834185
autoSettleAfterDays,
4186+
autoSettleOnMerge,
41844187
changeRequestState: activeThreadPr?.state ?? null,
41854188
});
41864189
}, [
41874190
activeThreadPr?.state,
41884191
activeThreadShell,
41894192
autoSettleAfterDays,
4193+
autoSettleOnMerge,
41904194
nowMinute,
41914195
supportsSettlement,
41924196
]);

0 commit comments

Comments
 (0)