| author | |
| committer | |
| log | e77f42c111dc40e0b208393957ebb23863330e2e |
| tree | d18baf32dbbfcf7e2cfe59054980a825764aea0b |
| parent | 6e3b73884bf40eeb2859cb16f1200c8e2f9cf7c3 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>6 files changed, 79 insertions(+), 16 deletions(-)
apps/web/src/components/BranchToolbar.tsx+1-1| ... | ... | @@ -234,10 +234,10 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 234 | 234 | () => scopeThreadRef(environmentId, threadId), |
| 235 | 235 | [environmentId, threadId], |
| 236 | 236 | ); |
| 237 | const serverThread = useThread(threadRef); | |
| 238 | 237 | const draftThread = useComposerDraftStore((store) => |
| 239 | 238 | draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), |
| 240 | 239 | ); |
| 240 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | |
| 241 | 241 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 242 | 242 | const activeProjectRef = serverThread |
| 243 | 243 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
apps/web/src/components/BranchToolbarBranchSelector.tsx+2-2| ... | ... | @@ -129,11 +129,11 @@ export function BranchToolbarBranchSelector({ |
| 129 | 129 | () => scopeThreadRef(environmentId, threadId), |
| 130 | 130 | [environmentId, threadId], |
| 131 | 131 | ); |
| 132 | const serverThread = useThread(threadRef); | |
| 133 | const serverSession = serverThread?.session ?? null; | |
| 134 | 132 | const draftThread = useComposerDraftStore((store) => |
| 135 | 133 | draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), |
| 136 | 134 | ); |
| 135 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | |
| 136 | const serverSession = serverThread?.session ?? null; | |
| 137 | 137 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 138 | 138 | |
| 139 | 139 | const activeProjectRef = serverThread |
apps/web/src/components/ChatView.tsx+10-10| ... | ... | @@ -620,8 +620,8 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra |
| 620 | 620 | const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); |
| 621 | 621 | const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); |
| 622 | 622 | const closeTerminalMutation = useAtomCommand(terminalEnvironment.close, "terminal close"); |
| 623 | const serverThread = useThread(threadRef); | |
| 624 | 623 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); |
| 624 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | |
| 625 | 625 | const projectRef = serverThread |
| 626 | 626 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
| 627 | 627 | : draftThread |
| ... | ... | @@ -976,8 +976,8 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane |
| 976 | 976 | newShortcutLabel, |
| 977 | 977 | closeShortcutLabel, |
| 978 | 978 | }: PersistentThreadTerminalPanelProps) { |
| 979 | const serverThread = useThread(threadRef); | |
| 980 | 979 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); |
| 980 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | |
| 981 | 981 | const projectRef = serverThread |
| 982 | 982 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
| 983 | 983 | : draftThread |
| ... | ... | @@ -1181,7 +1181,14 @@ function ChatViewContent(props: ChatViewProps) { |
| 1181 | 1181 | ); |
| 1182 | 1182 | const composerDraftTarget: ScopedThreadRef | DraftId = |
| 1183 | 1183 | routeKind === "server" ? routeThreadRef : props.draftId; |
| 1184 | const serverThread = useThread(routeThreadRef); | |
| 1184 | const draftThread = useComposerDraftStore((store) => | |
| 1185 | routeKind === "server" | |
| 1186 | ? store.getDraftSessionByRef(routeThreadRef) | |
| 1187 | : draftId | |
| 1188 | ? store.getDraftSession(draftId) | |
| 1189 | : null, | |
| 1190 | ); | |
| 1191 | const serverThread = useThread(routeThreadRef, { waitForShell: draftThread !== null }); | |
| 1185 | 1192 | const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); |
| 1186 | 1193 | const activeThreadLastVisitedAt = useUiStateStore( |
| 1187 | 1194 | (store) => store.threadLastVisitedAtById[routeThreadKey], |
| ... | ... | @@ -1234,13 +1241,6 @@ function ChatViewContent(props: ChatViewProps) { |
| 1234 | 1241 | const setLogicalProjectDraftThreadId = useComposerDraftStore( |
| 1235 | 1242 | (store) => store.setLogicalProjectDraftThreadId, |
| 1236 | 1243 | ); |
| 1237 | const draftThread = useComposerDraftStore((store) => | |
| 1238 | routeKind === "server" | |
| 1239 | ? store.getDraftSessionByRef(routeThreadRef) | |
| 1240 | : draftId | |
| 1241 | ? store.getDraftSession(draftId) | |
| 1242 | : null, | |
| 1243 | ); | |
| 1244 | 1244 | const promptRef = useRef(""); |
| 1245 | 1245 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 1246 | 1246 | const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]); |
apps/web/src/components/GitActionsControl.tsx+3-1| ... | ... | @@ -986,7 +986,6 @@ export default function GitActionsControl({ |
| 986 | 986 | () => (activeThreadRef ? { threadRef: activeThreadRef } : undefined), |
| 987 | 987 | [activeThreadRef], |
| 988 | 988 | ); |
| 989 | const activeServerThread = useThread(activeThreadRef); | |
| 990 | 989 | const activeDraftThread = useComposerDraftStore((store) => |
| 991 | 990 | draftId |
| 992 | 991 | ? store.getDraftSession(draftId) |
| ... | ... | @@ -994,6 +993,9 @@ export default function GitActionsControl({ |
| 994 | 993 | ? store.getDraftThreadByRef(activeThreadRef) |
| 995 | 994 | : null, |
| 996 | 995 | ); |
| 996 | const activeServerThread = useThread(activeThreadRef, { | |
| 997 | waitForShell: activeDraftThread !== null, | |
| 998 | }); | |
| 997 | 999 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 998 | 1000 | const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); |
| 999 | 1001 | const [dialogCommitMessage, setDialogCommitMessage] = useState(""); |
apps/web/src/state/entities.test.ts created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; | |
| 2 | import { EnvironmentId, ThreadId } from "@t3tools/contracts"; | |
| 3 | import { describe, expect, it } from "vite-plus/test"; | |
| 4 | ||
| 5 | import { resolveThreadDetailRef } from "./entities"; | |
| 6 | ||
| 7 | const threadRef = scopeThreadRef(EnvironmentId.make("environment-1"), ThreadId.make("thread-1")); | |
| 8 | ||
| 9 | describe("resolveThreadDetailRef", () => { | |
| 10 | it("does not subscribe to a reserved draft thread before it enters the shell index", () => { | |
| 11 | expect( | |
| 12 | resolveThreadDetailRef(threadRef, { | |
| 13 | shellExists: false, | |
| 14 | waitForShell: true, | |
| 15 | }), | |
| 16 | ).toBeNull(); | |
| 17 | }); | |
| 18 | ||
| 19 | it("subscribes once the reserved draft thread enters the shell index", () => { | |
| 20 | expect( | |
| 21 | resolveThreadDetailRef(threadRef, { | |
| 22 | shellExists: true, | |
| 23 | waitForShell: true, | |
| 24 | }), | |
| 25 | ).toBe(threadRef); | |
| 26 | }); | |
| 27 | ||
| 28 | it("keeps direct server-thread lookups enabled when the shell has not loaded it", () => { | |
| 29 | expect( | |
| 30 | resolveThreadDetailRef(threadRef, { | |
| 31 | shellExists: false, | |
| 32 | waitForShell: false, | |
| 33 | }), | |
| 34 | ).toBe(threadRef); | |
| 35 | }); | |
| 36 | }); |
apps/web/src/state/entities.ts+27-2| ... | ... | @@ -152,10 +152,35 @@ export function useThreadStatus(ref: ScopedThreadRef | null): EnvironmentThreadS |
| 152 | 152 | ); |
| 153 | 153 | } |
| 154 | 154 | |
| 155 | export function resolveThreadDetailRef( | |
| 156 | ref: ScopedThreadRef | null, | |
| 157 | options: { | |
| 158 | shellExists: boolean; | |
| 159 | waitForShell: boolean; | |
| 160 | }, | |
| 161 | ): ScopedThreadRef | null { | |
| 162 | return ref !== null && (!options.waitForShell || options.shellExists) ? ref : null; | |
| 163 | } | |
| 164 | ||
| 155 | 165 | /** Detail collections composed with shell-authoritative thread/workspace metadata. */ |
| 156 | export function useThread(ref: ScopedThreadRef | null): EnvironmentThread | null { | |
| 166 | export function useThread( | |
| 167 | ref: ScopedThreadRef | null, | |
| 168 | options?: { | |
| 169 | /** | |
| 170 | * Client-reserved draft thread ids do not exist on the server until the | |
| 171 | * first send. Waiting for the shell index avoids polling the detail | |
| 172 | * endpoint for an intentionally missing thread during that window. | |
| 173 | */ | |
| 174 | waitForShell?: boolean; | |
| 175 | }, | |
| 176 | ): EnvironmentThread | null { | |
| 157 | 177 | const shell = useThreadShell(ref); |
| 158 | const detail = useThreadDetail(ref); | |
| 178 | const detail = useThreadDetail( | |
| 179 | resolveThreadDetailRef(ref, { | |
| 180 | shellExists: shell !== null, | |
| 181 | waitForShell: options?.waitForShell === true, | |
| 182 | }), | |
| 183 | ); | |
| 159 | 184 | return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); |
| 160 | 185 | } |
| 161 | 186 |