From 506b808c283a9719250c5bc0757a55c3ee4f9f23 Mon Sep 17 00:00:00 2001 From: tyulyukov Date: Fri, 24 Apr 2026 14:37:08 +0300 Subject: [PATCH 1/2] fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the client-derived buildSidebarThreadSummary path that was overwriting server-authoritative sidebar flags during the window between a detail-stream write and the next shell event. Matches the stream- separation contract established in upstream #1996 and required by MEMORY.md ("ghost Pending Approval badges on resolved threads"). Changes in apps/web/src/store.ts: - Delete buildSidebarThreadSummary() + getLatestUserMessageAt() (only used by the broken path). - Remove session-logic imports: derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan (their tests in session-logic.test.ts still exercise them). - writeThreadState (detail stream): stop computing nextSummary; drop the sidebarThreadSummariesEqual block. Detail stream no longer touches sidebarThreadSummaryById. - buildThreadState (legacy read-model path): drop sidebarThreadSummaryById from the return type Pick; stop populating it. The shell stream is the sole writer. - Add doc comments to writeThreadState + writeThreadShellState explaining stream ownership, mirroring upstream #1996's guidance. Changes in apps/web/src/store.test.ts: - Update the "shell events are authoritative for sidebar summary flags" describe block's two tests to reflect the new contract: detail-stream events do NOT populate sidebarThreadSummaryById; shell events are the sole writer. Extended both tests to also verify the later-shell-event transition (true → false) still wins authoritatively. What we deliberately did NOT port from upstream #1996: - Structural reorganization of store.ts (initialEnvironmentState commentary, ensureThreadRegistered helper extraction). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers this functionally via syncServerReadModel. Dragging in upstream's cosmetic reshuffle risks regression for no behavioral win. --- apps/web/src/store.test.ts | 60 ++++++++++++++------- apps/web/src/store.ts | 104 ++++++++++++++++--------------------- 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index 63ca8271501b..d0d6ec5c2051 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -1386,9 +1386,10 @@ describe("shell events are authoritative for sidebar summary flags", () => { }; } - it("clears hasPendingUserInput when a later shell event reports the projection resolved it", () => { + it("detail-stream activity-appended event does not populate hasPendingUserInput; shell stream is the sole writer", () => { const thread = makeThread(); const state = makeState(thread); + const ref = scopeThreadRef(thread.environmentId, thread.id); const afterActivity = applyOrchestrationEvent( state, @@ -1423,23 +1424,31 @@ describe("shell events are authoritative for sidebar summary flags", () => { localEnvironmentId, ); - const ref = scopeThreadRef(thread.environmentId, thread.id); - const afterActivitySummary = selectSidebarThreadSummaryByRef(afterActivity, ref); - expect(afterActivitySummary?.hasPendingUserInput).toBe(true); - expect(resolveThreadStatusPill({ thread: afterActivitySummary! })?.label).toBe( - "Awaiting Input", - ); + // Detail stream must NOT write sidebarThreadSummaryById — shell stream owns it. + expect(selectSidebarThreadSummaryByRef(afterActivity, ref)).toBeUndefined(); - const afterShell = applyShellEvent( + // A shell event that reports the server-computed flag as TRUE populates the summary. + const afterShellWithPending = applyShellEvent( afterActivity, - makeThreadUpsertedShellEvent(thread), + makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: true }), localEnvironmentId, ); - const afterShellSummary = selectSidebarThreadSummaryByRef(afterShell, ref); - expect(afterShellSummary?.hasPendingUserInput).toBe(false); + const pendingSummary = selectSidebarThreadSummaryByRef(afterShellWithPending, ref); + expect(pendingSummary?.hasPendingUserInput).toBe(true); + expect(resolveThreadStatusPill({ thread: pendingSummary! })?.label).toBe("Awaiting Input"); + + // A later shell event with the flag cleared wins authoritatively. + const afterShellResolved = applyShellEvent( + afterShellWithPending, + makeThreadUpsertedShellEvent(thread, { hasPendingUserInput: false }), + localEnvironmentId, + ); + expect(selectSidebarThreadSummaryByRef(afterShellResolved, ref)?.hasPendingUserInput).toBe( + false, + ); }); - it("clears hasActionableProposedPlan when a later shell event reports the projection resolved it", () => { + it("detail-stream proposed-plan-upserted event does not populate hasActionableProposedPlan; shell stream is the sole writer", () => { const thread = makeThread({ interactionMode: "plan", latestTurn: { @@ -1452,6 +1461,7 @@ describe("shell events are authoritative for sidebar summary flags", () => { }, }); const state = makeState(thread); + const ref = scopeThreadRef(thread.environmentId, thread.id); const afterPlan = applyOrchestrationEvent( state, @@ -1470,17 +1480,27 @@ describe("shell events are authoritative for sidebar summary flags", () => { localEnvironmentId, ); - const ref = scopeThreadRef(thread.environmentId, thread.id); - const afterPlanSummary = selectSidebarThreadSummaryByRef(afterPlan, ref); - expect(afterPlanSummary?.hasActionableProposedPlan).toBe(true); - expect(resolveThreadStatusPill({ thread: afterPlanSummary! })?.label).toBe("Plan Ready"); + // Detail stream must NOT write sidebarThreadSummaryById — shell stream owns it. + expect(selectSidebarThreadSummaryByRef(afterPlan, ref)).toBeUndefined(); - const afterShell = applyShellEvent( + // A shell event that reports the server-computed flag as TRUE populates the summary. + const afterShellWithPlan = applyShellEvent( afterPlan, - makeThreadUpsertedShellEvent(thread), + makeThreadUpsertedShellEvent(thread, { hasActionableProposedPlan: true }), + localEnvironmentId, + ); + const planSummary = selectSidebarThreadSummaryByRef(afterShellWithPlan, ref); + expect(planSummary?.hasActionableProposedPlan).toBe(true); + expect(resolveThreadStatusPill({ thread: planSummary! })?.label).toBe("Plan Ready"); + + // A later shell event with the flag cleared wins authoritatively. + const afterShellResolved = applyShellEvent( + afterShellWithPlan, + makeThreadUpsertedShellEvent(thread, { hasActionableProposedPlan: false }), localEnvironmentId, ); - const afterShellSummary = selectSidebarThreadSummaryByRef(afterShell, ref); - expect(afterShellSummary?.hasActionableProposedPlan).toBe(false); + expect( + selectSidebarThreadSummaryByRef(afterShellResolved, ref)?.hasActionableProposedPlan, + ).toBe(false); }); }); diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 3804cbf31c49..6a8f14707752 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -23,12 +23,6 @@ import type { import { Schema } from "effect"; import { resolveModelSlugForProvider } from "@marcode/shared/model"; import { create } from "zustand"; -import { - derivePendingApprovals, - derivePendingUserInputs, - findLatestProposedPlan, - hasActionableProposedPlan, -} from "./session-logic"; import { type ChatMessage, type Project, @@ -309,42 +303,6 @@ function toThreadTurnState(thread: Thread): ThreadTurnState { }; } -function getLatestUserMessageAt(messages: ReadonlyArray): string | null { - let latestUserMessageAt: string | null = null; - for (const message of messages) { - if (message.role !== "user") { - continue; - } - if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { - latestUserMessageAt = message.createdAt; - } - } - return latestUserMessageAt; -} - -function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { - return { - id: thread.id, - environmentId: thread.environmentId, - projectId: thread.projectId, - title: thread.title, - interactionMode: thread.interactionMode, - session: thread.session, - createdAt: thread.createdAt, - archivedAt: thread.archivedAt, - updatedAt: thread.updatedAt, - latestTurn: thread.latestTurn, - branch: thread.branch, - worktreePath: thread.worktreePath, - latestUserMessageAt: getLatestUserMessageAt(thread.messages), - hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, - hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, - hasActionableProposedPlan: hasActionableProposedPlan( - findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), - ), - }; -} - function sidebarThreadSummariesEqual( left: SidebarThreadSummary | undefined, right: SidebarThreadSummary, @@ -486,6 +444,22 @@ function getThreads(state: EnvironmentState): Thread[] { }); } +/** + * Write thread state from the **detail stream** (single-thread subscription). + * + * Owns: messages, activities, proposedPlans, turn diff summaries. + * Also writes threadShellById / threadSessionById / threadTurnStateById so the + * active thread has up-to-date state even if the shell event hasn't arrived yet; + * structural equality checks below keep those writes from triggering re-renders + * when both streams deliver equivalent data. + * + * Does NOT write sidebarThreadSummaryById — that is shell-stream-only. The + * shell stream carries server-computed hasPendingApprovals / hasPendingUserInput / + * hasActionableProposedPlan / latestUserMessageAt fields; re-deriving them from + * `thread.activities` / `thread.messages` here would overwrite authoritative + * values and cause ghost "Pending Approval" badges on resolved threads (see + * MEMORY.md and upstream #1996). + */ function writeThreadState( state: EnvironmentState, nextThread: Thread, @@ -495,8 +469,6 @@ function writeThreadState( const nextTurnState = toThreadTurnState(nextThread); const previousShell = state.threadShellById[nextThread.id]; const previousTurnState = state.threadTurnStateById[nextThread.id]; - const previousSummary = state.sidebarThreadSummaryById[nextThread.id]; - const nextSummary = buildSidebarThreadSummary(nextThread); let nextState = state; @@ -630,16 +602,6 @@ function writeThreadState( }; } - if (!sidebarThreadSummariesEqual(previousSummary, nextSummary)) { - nextState = { - ...nextState, - sidebarThreadSummaryById: { - ...nextState.sidebarThreadSummaryById, - [nextThread.id]: nextSummary, - }, - }; - } - return nextState; } @@ -747,6 +709,23 @@ function ensureThreadRegistered( return nextState; } +/** + * Write thread state from the **shell stream** (all-threads subscription). + * + * Owns: sidebarThreadSummaryById (pre-computed server-side: hasPendingApprovals + * / hasPendingUserInput / hasActionableProposedPlan / latestUserMessageAt). This + * is the single source of truth for sidebar data. The detail stream MUST NOT + * write here — re-deriving the summary fields from detail state would cause + * ghost "Pending Approval" badges on resolved threads (MEMORY.md, upstream #1996). + * + * Also writes threadShellById / threadSessionById / threadTurnStateById as the + * authoritative source for those fields (the detail stream may also write them + * as fallback while shell events are in flight; structural equality checks in + * both writers prevent unnecessary re-renders). + * + * Does NOT write message/activity/proposedPlan/turnDiff content — that is + * detail-stream-only. + */ function writeThreadShellState( state: EnvironmentState, nextThread: { @@ -1041,6 +1020,19 @@ function buildProjectState( }; } +/** + * Build detail-stream state slice for a batch of threads (legacy read-model path). + * + * Owns: messages, activities, proposedPlans, turn diff summaries, plus + * threadShellById / threadSessionById / threadTurnStateById as fallback state + * (the shell stream is the authoritative writer for those three). + * + * Does NOT populate sidebarThreadSummaryById — the `Thread` type carries detail + * data (messages/activities/proposedPlans), not the server-computed summary + * fields (hasPendingApprovals / hasPendingUserInput / hasActionableProposedPlan + * / latestUserMessageAt) that live on OrchestrationThreadShell. The shell + * stream is the single source of truth for sidebar data. + */ function buildThreadState( threads: ReadonlyArray, ): Pick< @@ -1058,7 +1050,6 @@ function buildThreadState( | "proposedPlanByThreadId" | "turnDiffIdsByThreadId" | "turnDiffSummaryByThreadId" - | "sidebarThreadSummaryById" > { const threadIds: ThreadId[] = []; const threadIdsByProjectId: Record = {}; @@ -1073,7 +1064,6 @@ function buildThreadState( const proposedPlanByThreadId: Record> = {}; const turnDiffIdsByThreadId: Record = {}; const turnDiffSummaryByThreadId: Record> = {}; - const sidebarThreadSummaryById: Record = {}; for (const thread of threads) { threadIds.push(thread.id); @@ -1096,7 +1086,6 @@ function buildThreadState( const turnDiffSlice = buildTurnDiffSlice(thread); turnDiffIdsByThreadId[thread.id] = turnDiffSlice.ids; turnDiffSummaryByThreadId[thread.id] = turnDiffSlice.byId; - sidebarThreadSummaryById[thread.id] = buildSidebarThreadSummary(thread); } return { @@ -1113,7 +1102,6 @@ function buildThreadState( proposedPlanByThreadId, turnDiffIdsByThreadId, turnDiffSummaryByThreadId, - sidebarThreadSummaryById, }; } From 566e79549f0a9faa808e2272c2d68e54b489b5f2 Mon Sep 17 00:00:00 2001 From: tyulyukov Date: Fri, 24 Apr 2026 14:39:27 +0300 Subject: [PATCH 2/2] docs: correct UPSTREAM_DIVERGENCE.md entries for #1996 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split upstream #1996 into two ledger entries for the two MarCode PRs that port it: PR #69 (sidebar row timestamp behavioral fix, 524e93afd) and PR #70 (store.ts shell-stream-authority correctness refactor, 506b808c2). - Remove "NOT yet equivalent" note in the Already-equivalent section now that both halves are ported. - Drop #1996 from Pending real work; only #2246 remains. - Add plan-file pointer (/Users/tyulyukov/.claude/plans/cached-napping-sundae.md) to the #2246 row so the next cycle's porter has the staged-commit strategy. Corrects the earlier claim that #1996's store.ts refactor was "cleanup with no behavioral delta" — it's a real correctness fix aligning MarCode with the stream-separation contract MEMORY.md already requires. --- UPSTREAM_DIVERGENCE.md | 55 +++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/UPSTREAM_DIVERGENCE.md b/UPSTREAM_DIVERGENCE.md index 0a886c3f6e78..d61502de5dc1 100644 --- a/UPSTREAM_DIVERGENCE.md +++ b/UPSTREAM_DIVERGENCE.md @@ -82,27 +82,45 @@ Sections are ordered by action: - `toast.tsx` — took upstream's version wholesale (492 → 719 lines). Upstream already bundled `CopyErrorButton` at line 93, so no manual re-integration needed. - `Sidebar.tsx` — preserved MarCode's "Delete anyway" warning toast flow (action button → deferred close → `api.dialogs.confirm` with thread-count messaging → `removeProject({ force: true })` → inline error toast). Wrapped all toast calls via the new `stackedThreadToast(...)` helper for layout consistency. +### PR #69 — sidebar row timestamp (part 1 of upstream #1996) + +| Upstream | Subject | New SHA | +| ------------------------------------------------------ | -------------------------------------------------------- | ----------- | +| [#1996](https://github.com/pingdotgg/t3code/pull/1996) | Use latest user message time for thread timestamps (row) | `524e93afd` | + +Narrow behavioral port: `Sidebar.tsx:715` thread-row label now falls back `thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt`. Matches the helper already in `CommandPalette.logic.ts:161`. + +### PR #70 — shell-stream authority for sidebar summary (part 2 of upstream #1996) + +| Upstream | Subject | New SHA | +| ------------------------------------------------------ | ------------------------------------------------------------------- | ----------- | +| [#1996](https://github.com/pingdotgg/t3code/pull/1996) | Use latest user message time for thread timestamps (store refactor) | `506b808c2` | + +**Correctness fix, not cleanup.** Removes the client-derived `buildSidebarThreadSummary` path in `store.ts` that was overwriting server-authoritative sidebar flags (`hasPendingApprovals`, `hasPendingUserInput`, `hasActionableProposedPlan`, `latestUserMessageAt`) during every detail-stream write. Matches the stream-separation contract MEMORY.md requires (guards against "ghost Pending Approval badges on resolved threads"). Tests in `store.test.ts` describe block "shell events are authoritative for sidebar summary flags" rewritten to assert the new contract: detail stream must not touch sidebar; shell stream is the sole writer. + +What we deliberately did NOT port from upstream #1996: structural reorganization of `store.ts` (commentary, `ensureThreadRegistered` extraction, `retainThreadScopedRecord` changes). MarCode's Incremental Event Handling & Structural Sharing (FEATURES.md §1) already covers the functional behavior; upstream's cosmetic reshuffle is net risk for no gain. + --- ## Already equivalent under a different SHA These upstream PRs are **behaviorally present** in MarCode via non-identical patches. `git cherry` flags them as missing because patch-ids differ. **Do not re-port.** -| Upstream | Subject | MarCode equivalent | -| ------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1541](https://github.com/pingdotgg/t3code/pull/1541) | fix(claude): emit plan events for TodoWrite during input streaming | `f4719a05f feat(tool-activity): TodoWrite → plan sidebar + apply_patch → file-change for Cursor/OpenCode` — generalized via `@marcode/shared/toolActivity` with `isTodoWriteTool` + `extractPlanStepsFromTodos` re-exported in `ClaudeAdapter.ts:48` and also used in Cursor / OpenCode adapters. | -| [#1944](https://github.com/pingdotgg/t3code/pull/1944) | feat(web): add tooltip to composer file mention pill | Already present in `ComposerPromptEditor.tsx` from earlier merge. Cherry-pick was an empty diff except for an unrelated `PasteCommandType` import differing. | -| [#1953](https://github.com/pingdotgg/t3code/pull/1953) | Migrate chat scrolling and branch lists to LegendList | `f3670b839 perf(chat): restore LegendList virtualization for message timeline` — restored after an earlier merge lost it. | -| [#1996](https://github.com/pingdotgg/t3code/pull/1996) | Use latest user message time for thread timestamps | **NOT yet equivalent** — listed in [Pending real work](#pending-real-work). Only `917ab971b Remove unnecessary export from getThreadSortTimestamp` exists locally but doesn't carry the substantive behavior. | -| [#2001](https://github.com/pingdotgg/t3code/pull/2001) | Warm sidebar thread detail subscriptions | `e82a9cf52 feat(sidebar): restore thread snapshot prewarming` — different implementation (snapshot prewarm in Sidebar), achieves same UX goal. **Per [MEMORY.md]**: do NOT add `retainThreadDetailSubscription` to `ChatView.tsx`. | -| [#2002](https://github.com/pingdotgg/t3code/pull/2002) | Fix thread timeline autoscroll and simplify branch state | `fb41df9bd feat(timeline): auto-follow bottom when content grows` — ResizeObserver-based MarCode implementation. | -| [#2024](https://github.com/pingdotgg/t3code/pull/2024) | Add filesystem browse API and command palette project picker | `b9ef378e9 feat: unify add-project/add-folder on Cmd+K filesystem picker (upstream #2024)` — explicit port. | -| [#2055](https://github.com/pingdotgg/t3code/pull/2055) | feat: configurable project grouping | `6673ca823 feat(sidebar): configurable project grouping (port upstream #2055)` — explicit port + `3dae9e63b chore(web): port path normalization helpers (prereq)`. | -| [#2072](https://github.com/pingdotgg/t3code/pull/2072) | feat: add Claude Opus 4.7 to built-in models | `788194665` + `4c32418a2` + `18216a500` + `10bdc1225` — full stack including default-effort tweaks. | -| [#2099](https://github.com/pingdotgg/t3code/pull/2099) | guard against missing sidebarProjectGroupingOverrides | `c5f4c9115 fix(sidebar): handle undefined sidebarProjectGroupingOverrides on stale settings` — stricter version of the same fix (handles raw JSON decode). | -| [#2153](https://github.com/pingdotgg/t3code/pull/2153) | Redesign model picker with favorites and search | `41ddce8f0 feat(model-picker): port upstream sexy redesign with favorites and search` — explicit port. | -| [#2192](https://github.com/pingdotgg/t3code/pull/2192) | fix(server): prevent probeClaudeCapabilities from wasting API requests | Already present: `waitForAbortSignal` + `SDKUserMessage` never-yielding prompt in `ClaudeProvider.ts:485,514`. Cherry-pick diff is empty against our HEAD. | -| [#2255](https://github.com/pingdotgg/t3code/pull/2255) | fix(server): restore CODEX_HOME tilde expansion for Codex launches | `expandHomePath` already wired on `CodexProvider.ts:226` and `CodexSessionRuntime.ts:688` via [#2210](https://github.com/pingdotgg/t3code/pull/2210) + follow-ups (`63ea04e29`, `42afbb226`). | +| Upstream | Subject | MarCode equivalent | +| ------------------------------------------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1541](https://github.com/pingdotgg/t3code/pull/1541) | fix(claude): emit plan events for TodoWrite during input streaming | `f4719a05f feat(tool-activity): TodoWrite → plan sidebar + apply_patch → file-change for Cursor/OpenCode` — generalized via `@marcode/shared/toolActivity` with `isTodoWriteTool` + `extractPlanStepsFromTodos` re-exported in `ClaudeAdapter.ts:48` and also used in Cursor / OpenCode adapters. | +| [#1944](https://github.com/pingdotgg/t3code/pull/1944) | feat(web): add tooltip to composer file mention pill | Already present in `ComposerPromptEditor.tsx` from earlier merge. Cherry-pick was an empty diff except for an unrelated `PasteCommandType` import differing. | +| [#1953](https://github.com/pingdotgg/t3code/pull/1953) | Migrate chat scrolling and branch lists to LegendList | `f3670b839 perf(chat): restore LegendList virtualization for message timeline` — restored after an earlier merge lost it. | +| [#1996](https://github.com/pingdotgg/t3code/pull/1996) | Use latest user message time for thread timestamps | Ported across two PRs in this cycle: `524e93afd feat(sidebar): latestUserMessageAt for thread row timestamp (upstream #1996)` (PR #69, behavioral) + `506b808c2 fix(store): enforce shell-stream authority for sidebar summary flags (upstream #1996)` (PR #70, correctness refactor — deletes the detail-stream-derived `buildSidebarThreadSummary`). The earlier `917ab971b Remove unnecessary export from getThreadSortTimestamp` was unrelated prep. | +| [#2001](https://github.com/pingdotgg/t3code/pull/2001) | Warm sidebar thread detail subscriptions | `e82a9cf52 feat(sidebar): restore thread snapshot prewarming` — different implementation (snapshot prewarm in Sidebar), achieves same UX goal. **Per [MEMORY.md]**: do NOT add `retainThreadDetailSubscription` to `ChatView.tsx`. | +| [#2002](https://github.com/pingdotgg/t3code/pull/2002) | Fix thread timeline autoscroll and simplify branch state | `fb41df9bd feat(timeline): auto-follow bottom when content grows` — ResizeObserver-based MarCode implementation. | +| [#2024](https://github.com/pingdotgg/t3code/pull/2024) | Add filesystem browse API and command palette project picker | `b9ef378e9 feat: unify add-project/add-folder on Cmd+K filesystem picker (upstream #2024)` — explicit port. | +| [#2055](https://github.com/pingdotgg/t3code/pull/2055) | feat: configurable project grouping | `6673ca823 feat(sidebar): configurable project grouping (port upstream #2055)` — explicit port + `3dae9e63b chore(web): port path normalization helpers (prereq)`. | +| [#2072](https://github.com/pingdotgg/t3code/pull/2072) | feat: add Claude Opus 4.7 to built-in models | `788194665` + `4c32418a2` + `18216a500` + `10bdc1225` — full stack including default-effort tweaks. | +| [#2099](https://github.com/pingdotgg/t3code/pull/2099) | guard against missing sidebarProjectGroupingOverrides | `c5f4c9115 fix(sidebar): handle undefined sidebarProjectGroupingOverrides on stale settings` — stricter version of the same fix (handles raw JSON decode). | +| [#2153](https://github.com/pingdotgg/t3code/pull/2153) | Redesign model picker with favorites and search | `41ddce8f0 feat(model-picker): port upstream sexy redesign with favorites and search` — explicit port. | +| [#2192](https://github.com/pingdotgg/t3code/pull/2192) | fix(server): prevent probeClaudeCapabilities from wasting API requests | Already present: `waitForAbortSignal` + `SDKUserMessage` never-yielding prompt in `ClaudeProvider.ts:485,514`. Cherry-pick diff is empty against our HEAD. | +| [#2255](https://github.com/pingdotgg/t3code/pull/2255) | fix(server): restore CODEX_HOME tilde expansion for Codex launches | `expandHomePath` already wired on `CodexProvider.ts:226` and `CodexSessionRuntime.ts:688` via [#2210](https://github.com/pingdotgg/t3code/pull/2210) + follow-ups (`63ea04e29`, `42afbb226`). | **Verification strategy for re-checking in a later cycle:** grep for the symbol the upstream PR adds. If it's already in MarCode, confirm; do not cherry-pick. @@ -142,10 +160,9 @@ MarCode ships semver alphas (`1.0.0-alpha.*`), not nightly builds. Adopting nigh ## Pending real work -| Upstream | Subject | Risk | Notes | -| ------------------------------------------------------ | --------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1996](https://github.com/pingdotgg/t3code/pull/1996) | Use latest user message time for thread timestamps | Medium | Touches `store.ts` / `storeSelectors.ts` (FEATURES.md §"Incremental Event Handling & Structural Sharing"). 240-line test conflict on `store.test.ts`. | -| [#2246](https://github.com/pingdotgg/t3code/pull/2246) | Refactor provider model selections to option arrays | High | Adds migration `026_CanonicalizeModelSelectionOptions`. Touches every `*TextGeneration.ts` (collides with FEATURES.md §"Claude-Powered Text Generation") and collides with MarCode's provider instance registry work (`0e71e3023`, `42b428826`, `4da47be23`). Needs a dedicated branch + regression-guard test runs. | +| Upstream | Subject | Risk | Notes | +| ------------------------------------------------------ | --------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [#2246](https://github.com/pingdotgg/t3code/pull/2246) | Refactor provider model selections to option arrays | High | Adds migration `026_CanonicalizeModelSelectionOptions` (renumber to `030` — MarCode's `26_AuthSessionLastConnectedAt` already owns that id). Touches every `*TextGeneration.ts` (collides with FEATURES.md §"Claude-Powered Text Generation") and the MarCode provider instance registry work (`0e71e3023`, `42b428826`, `4da47be23`). 67 files, +3403/-3828. Plan: `/Users/tyulyukov/.claude/plans/cached-napping-sundae.md`. | ---