| author | |
| committer | |
| log | 18fa89c4adaae722e619f6abde093becf80efc08 |
| tree | ae47196c180750ef2d0db2c6b744ee9b18ac853a |
| parent | f74eb62661734927aea45a243e2c3effced43fb9 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; | ... | @@ -118,7 +118,7 @@ import { vcsEnvironment } from "../state/vcs"; |
| 118 | import { useEnvironment, useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; | 118 | import { useEnvironment, useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; |
| 119 | import { | 119 | import { |
| 120 | buildThreadRouteParams, | 120 | buildThreadRouteParams, |
| 121 | resolveThreadRouteRef, | 121 | resolveActiveThreadRouteRef, |
| 122 | resolveThreadRouteTarget, | 122 | resolveThreadRouteTarget, |
| 123 | } from "../threadRoutes"; | 123 | } from "../threadRoutes"; |
| 124 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 124 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| ... | @@ -3068,10 +3068,17 @@ export default function Sidebar() { | ... | @@ -3068,10 +3068,17 @@ export default function Sidebar() { |
| 3068 | const handleNewThread = useNewThreadHandler(); | 3068 | const handleNewThread = useNewThreadHandler(); |
| 3069 | const { archiveThread, deleteThread } = useThreadActions(); | 3069 | const { archiveThread, deleteThread } = useThreadActions(); |
| 3070 | const { isMobile, setOpenMobile } = useSidebar(); | 3070 | const { isMobile, setOpenMobile } = useSidebar(); |
| 3071 | const routeThreadRef = useParams({ | 3071 | const routeTarget = useParams({ |
| 3072 | strict: false, | 3072 | strict: false, |
| 3073 | select: (params) => resolveThreadRouteRef(params), | 3073 | select: (params) => resolveThreadRouteTarget(params), |
| 3074 | }); | 3074 | }); |
| 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 | ); | ||
| 3075 | const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null; | 3082 | const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null; |
| 3076 | const routeTerminalOpen = useTerminalUiStateStore((state) => | 3083 | const routeTerminalOpen = useTerminalUiStateStore((state) => |
| 3077 | routeThreadRef | 3084 | routeThreadRef |
apps/web/src/threadRoutes.test.ts+28| ... | @@ -6,6 +6,7 @@ import { DraftId } from "./composerDraftStore"; | ... | @@ -6,6 +6,7 @@ import { DraftId } from "./composerDraftStore"; |
| 6 | import { | 6 | import { |
| 7 | buildDraftThreadRouteParams, | 7 | buildDraftThreadRouteParams, |
| 8 | buildThreadRouteParams, | 8 | buildThreadRouteParams, |
| 9 | resolveActiveThreadRouteRef, | ||
| 9 | resolveThreadRouteRef, | 10 | resolveThreadRouteRef, |
| 10 | resolveThreadRouteTarget, | 11 | resolveThreadRouteTarget, |
| 11 | } from "./threadRoutes"; | 12 | } from "./threadRoutes"; |
| ... | @@ -64,4 +65,31 @@ describe("threadRoutes", () => { | ... | @@ -64,4 +65,31 @@ describe("threadRoutes", () => { |
| 64 | draftId: "draft-1", | 65 | draftId: "draft-1", |
| 65 | }); | 66 | }); |
| 66 | }); | 67 | }); |
| 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 | }); | ||
| 67 | }); | 95 | }); |
apps/web/src/threadRoutes.ts+23| ... | @@ -12,6 +12,12 @@ export type ThreadRouteTarget = | ... | @@ -12,6 +12,12 @@ export type ThreadRouteTarget = |
| 12 | draftId: DraftId; | 12 | draftId: DraftId; |
| 13 | }; | 13 | }; |
| 14 | 14 | ||
| 15 | type DraftThreadRouteState = { | ||
| 16 | environmentId: EnvironmentId; | ||
| 17 | threadId: ThreadId; | ||
| 18 | promotedTo?: ScopedThreadRef | null; | ||
| 19 | }; | ||
| 20 | |||
| 15 | export function buildThreadRouteParams(ref: ScopedThreadRef): { | 21 | export function buildThreadRouteParams(ref: ScopedThreadRef): { |
| 16 | environmentId: EnvironmentId; | 22 | environmentId: EnvironmentId; |
| 17 | threadId: ThreadId; | 23 | threadId: ThreadId; |
| ... | @@ -57,3 +63,20 @@ export function resolveThreadRouteTarget( | ... | @@ -57,3 +63,20 @@ export function resolveThreadRouteTarget( |
| 57 | draftId: params.draftId as DraftId, | 63 | draftId: params.draftId as DraftId, |
| 58 | }; | 64 | }; |
| 59 | } | 65 | } |
| 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 | */ | ||
| 71 | export 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 | } |