authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-22 17:26:41+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-22 17:26:41+02:00
log18fa89c4adaae722e619f6abde093becf80efc08
treeae47196c180750ef2d0db2c6b744ee9b18ac853a
parentf74eb62661734927aea45a243e2c3effced43fb9
signature Signed by PGP key B5690EEEBB952194

Preserve draft thread highlighting during promotion (#4283)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 61 insertions(+), 3 deletions(-)

apps/web/src/components/Sidebar.tsx+10-3
......@@ -118,7 +118,7 @@ import { vcsEnvironment } from "../state/vcs";
118118import { useEnvironment, useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
119119import {
120120 buildThreadRouteParams,
121 resolveThreadRouteRef,
121 resolveActiveThreadRouteRef,
122122 resolveThreadRouteTarget,
123123} from "../threadRoutes";
124124import { stackedThreadToast, toastManager } from "./ui/toast";
......@@ -3068,10 +3068,17 @@ export default function Sidebar() {
30683068 const handleNewThread = useNewThreadHandler();
30693069 const { archiveThread, deleteThread } = useThreadActions();
30703070 const { isMobile, setOpenMobile } = useSidebar();
3071 const routeThreadRef = useParams({
3071 const routeTarget = useParams({
30723072 strict: false,
3073 select: (params) => resolveThreadRouteRef(params),
3073 select: (params) => resolveThreadRouteTarget(params),
30743074 });
3075 const routeDraftThread = useComposerDraftStore((store) =>
3076 routeTarget?.kind === "draft" ? store.getDraftSession(routeTarget.draftId) : null,
3077 );
3078 const routeThreadRef = useMemo(
3079 () => resolveActiveThreadRouteRef(routeTarget, routeDraftThread),
3080 [routeDraftThread, routeTarget],
3081 );
30753082 const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null;
30763083 const routeTerminalOpen = useTerminalUiStateStore((state) =>
30773084 routeThreadRef
apps/web/src/threadRoutes.test.ts+28
......@@ -6,6 +6,7 @@ import { DraftId } from "./composerDraftStore";
66import {
77 buildDraftThreadRouteParams,
88 buildThreadRouteParams,
9 resolveActiveThreadRouteRef,
910 resolveThreadRouteRef,
1011 resolveThreadRouteTarget,
1112} from "./threadRoutes";
......@@ -64,4 +65,31 @@ describe("threadRoutes", () => {
6465 draftId: "draft-1",
6566 });
6667 });
68
69 it("resolves the backing thread while a draft route is being promoted", () => {
70 const target = resolveThreadRouteTarget({ draftId: "draft-1" });
71
72 expect(
73 resolveActiveThreadRouteRef(target, {
74 environmentId: "env-1" as never,
75 threadId: ThreadId.make("draft-thread"),
76 promotedTo: scopeThreadRef("env-2" as never, ThreadId.make("server-thread")),
77 }),
78 ).toEqual({
79 environmentId: "env-2",
80 threadId: "server-thread",
81 });
82 });
83
84 it("does not treat a draft's reserved thread ref as an active sidebar thread", () => {
85 const target = resolveThreadRouteTarget({ draftId: "draft-1" });
86
87 expect(
88 resolveActiveThreadRouteRef(target, {
89 environmentId: "env-1" as never,
90 threadId: ThreadId.make("draft-thread"),
91 promotedTo: null,
92 }),
93 ).toBeNull();
94 });
6795});
apps/web/src/threadRoutes.ts+23
......@@ -12,6 +12,12 @@ export type ThreadRouteTarget =
1212 draftId: DraftId;
1313 };
1414
15type DraftThreadRouteState = {
16 environmentId: EnvironmentId;
17 threadId: ThreadId;
18 promotedTo?: ScopedThreadRef | null;
19};
20
1521export function buildThreadRouteParams(ref: ScopedThreadRef): {
1622 environmentId: EnvironmentId;
1723 threadId: ThreadId;
......@@ -57,3 +63,20 @@ export function resolveThreadRouteTarget(
5763 draftId: params.draftId as DraftId,
5864 };
5965}
66
67/**
68 * Resolves the thread represented by either a canonical thread route or a
69 * draft route whose promotion to a server thread has been recorded.
70 */
71export function resolveActiveThreadRouteRef(
72 target: ThreadRouteTarget | null,
73 draftThread: DraftThreadRouteState | null,
74): ScopedThreadRef | null {
75 if (target?.kind === "server") {
76 return target.threadRef;
77 }
78 if (target?.kind !== "draft" || !draftThread?.promotedTo) {
79 return null;
80 }
81 return draftThread.promotedTo;
82}