| 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,10 +234,10 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 234 | () => scopeThreadRef(environmentId, threadId), | 234 | () => scopeThreadRef(environmentId, threadId), |
| 235 | [environmentId, threadId], | 235 | [environmentId, threadId], |
| 236 | ); | 236 | ); |
| 237 | const serverThread = useThread(threadRef); | ||
| 238 | const draftThread = useComposerDraftStore((store) => | 237 | const draftThread = useComposerDraftStore((store) => |
| 239 | draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), | 238 | draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), |
| 240 | ); | 239 | ); |
| 240 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | ||
| 241 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | 241 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 242 | const activeProjectRef = serverThread | 242 | const activeProjectRef = serverThread |
| 243 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) | 243 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
apps/web/src/components/BranchToolbarBranchSelector.tsx+2-2| ... | @@ -129,11 +129,11 @@ export function BranchToolbarBranchSelector({ | ... | @@ -129,11 +129,11 @@ export function BranchToolbarBranchSelector({ |
| 129 | () => scopeThreadRef(environmentId, threadId), | 129 | () => scopeThreadRef(environmentId, threadId), |
| 130 | [environmentId, threadId], | 130 | [environmentId, threadId], |
| 131 | ); | 131 | ); |
| 132 | const serverThread = useThread(threadRef); | ||
| 133 | const serverSession = serverThread?.session ?? null; | ||
| 134 | const draftThread = useComposerDraftStore((store) => | 132 | const draftThread = useComposerDraftStore((store) => |
| 135 | draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), | 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 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | 137 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 138 | 138 | ||
| 139 | const activeProjectRef = serverThread | 139 | const activeProjectRef = serverThread |
apps/web/src/components/ChatView.tsx+10-10| ... | @@ -620,8 +620,8 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra | ... | @@ -620,8 +620,8 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra |
| 620 | const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); | 620 | const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); |
| 621 | const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); | 621 | const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); |
| 622 | const closeTerminalMutation = useAtomCommand(terminalEnvironment.close, "terminal close"); | 622 | const closeTerminalMutation = useAtomCommand(terminalEnvironment.close, "terminal close"); |
| 623 | const serverThread = useThread(threadRef); | ||
| 624 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); | 623 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); |
| 624 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | ||
| 625 | const projectRef = serverThread | 625 | const projectRef = serverThread |
| 626 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) | 626 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
| 627 | : draftThread | 627 | : draftThread |
| ... | @@ -976,8 +976,8 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane | ... | @@ -976,8 +976,8 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane |
| 976 | newShortcutLabel, | 976 | newShortcutLabel, |
| 977 | closeShortcutLabel, | 977 | closeShortcutLabel, |
| 978 | }: PersistentThreadTerminalPanelProps) { | 978 | }: PersistentThreadTerminalPanelProps) { |
| 979 | const serverThread = useThread(threadRef); | ||
| 980 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); | 979 | const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); |
| 980 | const serverThread = useThread(threadRef, { waitForShell: draftThread !== null }); | ||
| 981 | const projectRef = serverThread | 981 | const projectRef = serverThread |
| 982 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) | 982 | ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) |
| 983 | : draftThread | 983 | : draftThread |
| ... | @@ -1181,7 +1181,14 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -1181,7 +1181,14 @@ function ChatViewContent(props: ChatViewProps) { |
| 1181 | ); | 1181 | ); |
| 1182 | const composerDraftTarget: ScopedThreadRef | DraftId = | 1182 | const composerDraftTarget: ScopedThreadRef | DraftId = |
| 1183 | routeKind === "server" ? routeThreadRef : props.draftId; | 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 | const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); | 1192 | const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); |
| 1186 | const activeThreadLastVisitedAt = useUiStateStore( | 1193 | const activeThreadLastVisitedAt = useUiStateStore( |
| 1187 | (store) => store.threadLastVisitedAtById[routeThreadKey], | 1194 | (store) => store.threadLastVisitedAtById[routeThreadKey], |
| ... | @@ -1234,13 +1241,6 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -1234,13 +1241,6 @@ function ChatViewContent(props: ChatViewProps) { |
| 1234 | const setLogicalProjectDraftThreadId = useComposerDraftStore( | 1241 | const setLogicalProjectDraftThreadId = useComposerDraftStore( |
| 1235 | (store) => store.setLogicalProjectDraftThreadId, | 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 | const promptRef = useRef(""); | 1244 | const promptRef = useRef(""); |
| 1245 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); | 1245 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 1246 | const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]); | 1246 | const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]); |
apps/web/src/components/GitActionsControl.tsx+3-1| ... | @@ -986,7 +986,6 @@ export default function GitActionsControl({ | ... | @@ -986,7 +986,6 @@ export default function GitActionsControl({ |
| 986 | () => (activeThreadRef ? { threadRef: activeThreadRef } : undefined), | 986 | () => (activeThreadRef ? { threadRef: activeThreadRef } : undefined), |
| 987 | [activeThreadRef], | 987 | [activeThreadRef], |
| 988 | ); | 988 | ); |
| 989 | const activeServerThread = useThread(activeThreadRef); | ||
| 990 | const activeDraftThread = useComposerDraftStore((store) => | 989 | const activeDraftThread = useComposerDraftStore((store) => |
| 991 | draftId | 990 | draftId |
| 992 | ? store.getDraftSession(draftId) | 991 | ? store.getDraftSession(draftId) |
| ... | @@ -994,6 +993,9 @@ export default function GitActionsControl({ | ... | @@ -994,6 +993,9 @@ export default function GitActionsControl({ |
| 994 | ? store.getDraftThreadByRef(activeThreadRef) | 993 | ? store.getDraftThreadByRef(activeThreadRef) |
| 995 | : null, | 994 | : null, |
| 996 | ); | 995 | ); |
| 996 | const activeServerThread = useThread(activeThreadRef, { | ||
| 997 | waitForShell: activeDraftThread !== null, | ||
| 998 | }); | ||
| 997 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | 999 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 998 | const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); | 1000 | const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); |
| 999 | const [dialogCommitMessage, setDialogCommitMessage] = useState(""); | 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,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 | /** Detail collections composed with shell-authoritative thread/workspace metadata. */ | 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 | const shell = useThreadShell(ref); | 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 | return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); | 184 | return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); |
| 160 | } | 185 | } |
| 161 | 186 |