| author | |
| committer | |
| log | bdf99c17bf7ae634728481677cf1bca8db65d00e |
| tree | b7ed96368cc3e3d0e173e7be2c95c8baf62106c7 |
| parent | 108e01746c585d98703df5fe76cb6c213ecb6b8f |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 68 insertions(+), 10 deletions(-)
apps/web/src/components/ChatView.logic.test.ts+28| ... | ... | @@ -25,6 +25,7 @@ import { |
| 25 | 25 | reconcileRetainedMountedThreadIds, |
| 26 | 26 | resolveThreadMetadataUpdateForNextTurn, |
| 27 | 27 | resolveSendEnvMode, |
| 28 | startNewThreadForProject, | |
| 28 | 29 | shouldShowBranchMismatchBanner, |
| 29 | 30 | shouldWriteThreadErrorToCurrentServerThread, |
| 30 | 31 | } from "./ChatView.logic"; |
| ... | ... | @@ -445,6 +446,33 @@ describe("shouldWriteThreadErrorToCurrentServerThread", () => { |
| 445 | 446 | }); |
| 446 | 447 | }); |
| 447 | 448 | |
| 449 | describe("startNewThreadForProject", () => { | |
| 450 | it("starts a thread through the supplied shared handler for the active project", () => { | |
| 451 | const calls: Array<{ environmentId: EnvironmentId; projectId: ProjectId }> = []; | |
| 452 | const projectRef = { environmentId, projectId }; | |
| 453 | ||
| 454 | expect( | |
| 455 | startNewThreadForProject(projectRef, (nextProjectRef) => { | |
| 456 | calls.push(nextProjectRef); | |
| 457 | return Promise.resolve(); | |
| 458 | }), | |
| 459 | ).toBe(true); | |
| 460 | expect(calls).toEqual([projectRef]); | |
| 461 | }); | |
| 462 | ||
| 463 | it("does nothing when the active project is unavailable", () => { | |
| 464 | let called = false; | |
| 465 | ||
| 466 | expect( | |
| 467 | startNewThreadForProject(null, () => { | |
| 468 | called = true; | |
| 469 | return Promise.resolve(); | |
| 470 | }), | |
| 471 | ).toBe(false); | |
| 472 | expect(called).toBe(false); | |
| 473 | }); | |
| 474 | }); | |
| 475 | ||
| 448 | 476 | describe("hasServerAcknowledgedLocalDispatch", () => { |
| 449 | 477 | it("does not acknowledge unchanged server state", () => { |
| 450 | 478 | const localDispatch = createLocalDispatchSnapshot( |
apps/web/src/components/ChatView.logic.ts+11| ... | ... | @@ -5,6 +5,7 @@ import { |
| 5 | 5 | type ModelSelection, |
| 6 | 6 | type ProviderDriverKind, |
| 7 | 7 | type ServerProvider, |
| 8 | type ScopedProjectRef, | |
| 8 | 9 | type ScopedThreadRef, |
| 9 | 10 | type ThreadId, |
| 10 | 11 | type TurnId, |
| ... | ... | @@ -27,6 +28,16 @@ export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3; |
| 27 | 28 | |
| 28 | 29 | export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); |
| 29 | 30 | |
| 31 | export function startNewThreadForProject( | |
| 32 | projectRef: ScopedProjectRef | null, | |
| 33 | handleNewThread: (projectRef: ScopedProjectRef) => Promise<void>, | |
| 34 | ): boolean { | |
| 35 | if (projectRef === null) return false; | |
| 36 | void handleNewThread(projectRef); | |
| 37 | ||
| 38 | return true; | |
| 39 | } | |
| 40 | ||
| 30 | 41 | export function resolveThreadMetadataUpdateForNextTurn(input: { |
| 31 | 42 | currentModelSelection: ModelSelection; |
| 32 | 43 | nextModelSelection?: ModelSelection; |
apps/web/src/components/ChatView.tsx+7| ... | ... | @@ -168,6 +168,7 @@ import { getProviderModelCapabilities, resolveSelectableProvider } from "../prov |
| 168 | 168 | import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; |
| 169 | 169 | import { useClientSettings, useEnvironmentSettings } from "../hooks/useSettings"; |
| 170 | 170 | import { useNowMinute } from "../hooks/useNowMinute"; |
| 171 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; | |
| 171 | 172 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 172 | 173 | import { getTerminalFocusOwner } from "../lib/terminalFocus"; |
| 173 | 174 | import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; |
| ... | ... | @@ -271,6 +272,7 @@ import { |
| 271 | 272 | resolveSendEnvMode, |
| 272 | 273 | revokeBlobPreviewUrl, |
| 273 | 274 | revokeUserMessagePreviewUrls, |
| 275 | startNewThreadForProject, | |
| 274 | 276 | waitForStartedServerThread, |
| 275 | 277 | } from "./ChatView.logic"; |
| 276 | 278 | import { useLocalStorage } from "~/hooks/useLocalStorage"; |
| ... | ... | @@ -1130,6 +1132,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1130 | 1132 | forceExpandedMobileComposer = false, |
| 1131 | 1133 | } = props; |
| 1132 | 1134 | const draftId = routeKind === "draft" ? props.draftId : null; |
| 1135 | const handleNewThread = useNewThreadHandler(); | |
| 1133 | 1136 | const routeThreadRef = useMemo( |
| 1134 | 1137 | () => scopeThreadRef(environmentId, threadId), |
| 1135 | 1138 | [environmentId, threadId], |
| ... | ... | @@ -1583,6 +1586,9 @@ function ChatViewContent(props: ChatViewProps) { |
| 1583 | 1586 | ? scopeProjectRef(activeThread.environmentId, activeThread.projectId) |
| 1584 | 1587 | : null; |
| 1585 | 1588 | const activeProject = useProject(activeProjectRef); |
| 1589 | const handleNewThreadInActiveProject = useCallback(() => { | |
| 1590 | startNewThreadForProject(activeProjectRef, handleNewThread); | |
| 1591 | }, [activeProjectRef, handleNewThread]); | |
| 1586 | 1592 | const activeEnvironmentShell = useEnvironmentQuery( |
| 1587 | 1593 | activeThread ? environmentShell.stateAtom(activeThread.environmentId) : null, |
| 1588 | 1594 | ); |
| ... | ... | @@ -5674,6 +5680,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5674 | 5680 | availableEditors={availableEditors} |
| 5675 | 5681 | rightPanelOpen={rightPanelOpen} |
| 5676 | 5682 | gitCwd={gitCwd} |
| 5683 | onNewThreadInProject={handleNewThreadInActiveProject} | |
| 5677 | 5684 | onRunProjectScript={runProjectScript} |
| 5678 | 5685 | onAddProjectScript={saveProjectScript} |
| 5679 | 5686 | onUpdateProjectScript={updateProjectScript} |
apps/web/src/components/chat/ChatHeader.tsx+22-10| ... | ... | @@ -34,6 +34,7 @@ interface ChatHeaderProps { |
| 34 | 34 | availableEditors: ReadonlyArray<EditorId>; |
| 35 | 35 | rightPanelOpen: boolean; |
| 36 | 36 | gitCwd: string | null; |
| 37 | onNewThreadInProject: () => void; | |
| 37 | 38 | onRunProjectScript: (script: ProjectScript) => void; |
| 38 | 39 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; |
| 39 | 40 | onUpdateProjectScript: ( |
| ... | ... | @@ -69,6 +70,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 69 | 70 | availableEditors, |
| 70 | 71 | rightPanelOpen, |
| 71 | 72 | gitCwd, |
| 73 | onNewThreadInProject, | |
| 72 | 74 | onRunProjectScript, |
| 73 | 75 | onAddProjectScript, |
| 74 | 76 | onUpdateProjectScript, |
| ... | ... | @@ -92,16 +94,26 @@ export const ChatHeader = memo(function ChatHeader({ |
| 92 | 94 | doesn't answer it. */} |
| 93 | 95 | {activeProjectName ? ( |
| 94 | 96 | <span className="inline-flex shrink-0 items-center gap-2"> |
| 95 | <span className="inline-flex min-w-0 items-center gap-1.5"> | |
| 96 | <ProjectFavicon | |
| 97 | environmentId={activeThreadEnvironmentId} | |
| 98 | cwd={activeProjectCwd ?? ""} | |
| 99 | className="size-3.5" | |
| 100 | /> | |
| 101 | <span className="max-w-40 truncate text-sm font-medium text-muted-foreground"> | |
| 102 | {activeProjectName} | |
| 103 | </span> | |
| 104 | </span> | |
| 97 | <Tooltip> | |
| 98 | <TooltipTrigger | |
| 99 | render={ | |
| 100 | <button | |
| 101 | type="button" | |
| 102 | aria-label={`New thread in ${activeProjectName}`} | |
| 103 | onClick={onNewThreadInProject} | |
| 104 | className="inline-flex min-w-0 cursor-pointer items-center gap-1.5 rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| 105 | /> | |
| 106 | } | |
| 107 | > | |
| 108 | <ProjectFavicon | |
| 109 | environmentId={activeThreadEnvironmentId} | |
| 110 | cwd={activeProjectCwd ?? ""} | |
| 111 | className="size-3.5" | |
| 112 | /> | |
| 113 | <span className="max-w-40 truncate text-sm font-medium">{activeProjectName}</span> | |
| 114 | </TooltipTrigger> | |
| 115 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> | |
| 116 | </Tooltip> | |
| 105 | 117 | <span aria-hidden className="text-muted-foreground/40"> |
| 106 | 118 | / |
| 107 | 119 | </span> |