| 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,6 +25,7 @@ import { |
| 25 | reconcileRetainedMountedThreadIds, | 25 | reconcileRetainedMountedThreadIds, |
| 26 | resolveThreadMetadataUpdateForNextTurn, | 26 | resolveThreadMetadataUpdateForNextTurn, |
| 27 | resolveSendEnvMode, | 27 | resolveSendEnvMode, |
| 28 | startNewThreadForProject, | ||
| 28 | shouldShowBranchMismatchBanner, | 29 | shouldShowBranchMismatchBanner, |
| 29 | shouldWriteThreadErrorToCurrentServerThread, | 30 | shouldWriteThreadErrorToCurrentServerThread, |
| 30 | } from "./ChatView.logic"; | 31 | } from "./ChatView.logic"; |
| ... | @@ -445,6 +446,33 @@ describe("shouldWriteThreadErrorToCurrentServerThread", () => { | ... | @@ -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 | describe("hasServerAcknowledgedLocalDispatch", () => { | 476 | describe("hasServerAcknowledgedLocalDispatch", () => { |
| 449 | it("does not acknowledge unchanged server state", () => { | 477 | it("does not acknowledge unchanged server state", () => { |
| 450 | const localDispatch = createLocalDispatchSnapshot( | 478 | const localDispatch = createLocalDispatchSnapshot( |
apps/web/src/components/ChatView.logic.ts+11| ... | @@ -5,6 +5,7 @@ import { | ... | @@ -5,6 +5,7 @@ import { |
| 5 | type ModelSelection, | 5 | type ModelSelection, |
| 6 | type ProviderDriverKind, | 6 | type ProviderDriverKind, |
| 7 | type ServerProvider, | 7 | type ServerProvider, |
| 8 | type ScopedProjectRef, | ||
| 8 | type ScopedThreadRef, | 9 | type ScopedThreadRef, |
| 9 | type ThreadId, | 10 | type ThreadId, |
| 10 | type TurnId, | 11 | type TurnId, |
| ... | @@ -27,6 +28,16 @@ export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3; | ... | @@ -27,6 +28,16 @@ export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3; |
| 27 | 28 | ||
| 28 | export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); | 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 | export function resolveThreadMetadataUpdateForNextTurn(input: { | 41 | export function resolveThreadMetadataUpdateForNextTurn(input: { |
| 31 | currentModelSelection: ModelSelection; | 42 | currentModelSelection: ModelSelection; |
| 32 | nextModelSelection?: ModelSelection; | 43 | nextModelSelection?: ModelSelection; |
apps/web/src/components/ChatView.tsx+7| ... | @@ -168,6 +168,7 @@ import { getProviderModelCapabilities, resolveSelectableProvider } from "../prov | ... | @@ -168,6 +168,7 @@ import { getProviderModelCapabilities, resolveSelectableProvider } from "../prov |
| 168 | import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; | 168 | import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; |
| 169 | import { useClientSettings, useEnvironmentSettings } from "../hooks/useSettings"; | 169 | import { useClientSettings, useEnvironmentSettings } from "../hooks/useSettings"; |
| 170 | import { useNowMinute } from "../hooks/useNowMinute"; | 170 | import { useNowMinute } from "../hooks/useNowMinute"; |
| 171 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; | ||
| 171 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; | 172 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 172 | import { getTerminalFocusOwner } from "../lib/terminalFocus"; | 173 | import { getTerminalFocusOwner } from "../lib/terminalFocus"; |
| 173 | import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; | 174 | import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions"; |
| ... | @@ -271,6 +272,7 @@ import { | ... | @@ -271,6 +272,7 @@ import { |
| 271 | resolveSendEnvMode, | 272 | resolveSendEnvMode, |
| 272 | revokeBlobPreviewUrl, | 273 | revokeBlobPreviewUrl, |
| 273 | revokeUserMessagePreviewUrls, | 274 | revokeUserMessagePreviewUrls, |
| 275 | startNewThreadForProject, | ||
| 274 | waitForStartedServerThread, | 276 | waitForStartedServerThread, |
| 275 | } from "./ChatView.logic"; | 277 | } from "./ChatView.logic"; |
| 276 | import { useLocalStorage } from "~/hooks/useLocalStorage"; | 278 | import { useLocalStorage } from "~/hooks/useLocalStorage"; |
| ... | @@ -1130,6 +1132,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -1130,6 +1132,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1130 | forceExpandedMobileComposer = false, | 1132 | forceExpandedMobileComposer = false, |
| 1131 | } = props; | 1133 | } = props; |
| 1132 | const draftId = routeKind === "draft" ? props.draftId : null; | 1134 | const draftId = routeKind === "draft" ? props.draftId : null; |
| 1135 | const handleNewThread = useNewThreadHandler(); | ||
| 1133 | const routeThreadRef = useMemo( | 1136 | const routeThreadRef = useMemo( |
| 1134 | () => scopeThreadRef(environmentId, threadId), | 1137 | () => scopeThreadRef(environmentId, threadId), |
| 1135 | [environmentId, threadId], | 1138 | [environmentId, threadId], |
| ... | @@ -1583,6 +1586,9 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -1583,6 +1586,9 @@ function ChatViewContent(props: ChatViewProps) { |
| 1583 | ? scopeProjectRef(activeThread.environmentId, activeThread.projectId) | 1586 | ? scopeProjectRef(activeThread.environmentId, activeThread.projectId) |
| 1584 | : null; | 1587 | : null; |
| 1585 | const activeProject = useProject(activeProjectRef); | 1588 | const activeProject = useProject(activeProjectRef); |
| 1589 | const handleNewThreadInActiveProject = useCallback(() => { | ||
| 1590 | startNewThreadForProject(activeProjectRef, handleNewThread); | ||
| 1591 | }, [activeProjectRef, handleNewThread]); | ||
| 1586 | const activeEnvironmentShell = useEnvironmentQuery( | 1592 | const activeEnvironmentShell = useEnvironmentQuery( |
| 1587 | activeThread ? environmentShell.stateAtom(activeThread.environmentId) : null, | 1593 | activeThread ? environmentShell.stateAtom(activeThread.environmentId) : null, |
| 1588 | ); | 1594 | ); |
| ... | @@ -5674,6 +5680,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5674,6 +5680,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5674 | availableEditors={availableEditors} | 5680 | availableEditors={availableEditors} |
| 5675 | rightPanelOpen={rightPanelOpen} | 5681 | rightPanelOpen={rightPanelOpen} |
| 5676 | gitCwd={gitCwd} | 5682 | gitCwd={gitCwd} |
| 5683 | onNewThreadInProject={handleNewThreadInActiveProject} | ||
| 5677 | onRunProjectScript={runProjectScript} | 5684 | onRunProjectScript={runProjectScript} |
| 5678 | onAddProjectScript={saveProjectScript} | 5685 | onAddProjectScript={saveProjectScript} |
| 5679 | onUpdateProjectScript={updateProjectScript} | 5686 | onUpdateProjectScript={updateProjectScript} |
apps/web/src/components/chat/ChatHeader.tsx+22-10| ... | @@ -34,6 +34,7 @@ interface ChatHeaderProps { | ... | @@ -34,6 +34,7 @@ interface ChatHeaderProps { |
| 34 | availableEditors: ReadonlyArray<EditorId>; | 34 | availableEditors: ReadonlyArray<EditorId>; |
| 35 | rightPanelOpen: boolean; | 35 | rightPanelOpen: boolean; |
| 36 | gitCwd: string | null; | 36 | gitCwd: string | null; |
| 37 | onNewThreadInProject: () => void; | ||
| 37 | onRunProjectScript: (script: ProjectScript) => void; | 38 | onRunProjectScript: (script: ProjectScript) => void; |
| 38 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; | 39 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; |
| 39 | onUpdateProjectScript: ( | 40 | onUpdateProjectScript: ( |
| ... | @@ -69,6 +70,7 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -69,6 +70,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 69 | availableEditors, | 70 | availableEditors, |
| 70 | rightPanelOpen, | 71 | rightPanelOpen, |
| 71 | gitCwd, | 72 | gitCwd, |
| 73 | onNewThreadInProject, | ||
| 72 | onRunProjectScript, | 74 | onRunProjectScript, |
| 73 | onAddProjectScript, | 75 | onAddProjectScript, |
| 74 | onUpdateProjectScript, | 76 | onUpdateProjectScript, |
| ... | @@ -92,16 +94,26 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -92,16 +94,26 @@ export const ChatHeader = memo(function ChatHeader({ |
| 92 | doesn't answer it. */} | 94 | doesn't answer it. */} |
| 93 | {activeProjectName ? ( | 95 | {activeProjectName ? ( |
| 94 | <span className="inline-flex shrink-0 items-center gap-2"> | 96 | <span className="inline-flex shrink-0 items-center gap-2"> |
| 95 | <span className="inline-flex min-w-0 items-center gap-1.5"> | 97 | <Tooltip> |
| 96 | <ProjectFavicon | 98 | <TooltipTrigger |
| 97 | environmentId={activeThreadEnvironmentId} | 99 | render={ |
| 98 | cwd={activeProjectCwd ?? ""} | 100 | <button |
| 99 | className="size-3.5" | 101 | type="button" |
| 100 | /> | 102 | aria-label={`New thread in ${activeProjectName}`} |
| 101 | <span className="max-w-40 truncate text-sm font-medium text-muted-foreground"> | 103 | onClick={onNewThreadInProject} |
| 102 | {activeProjectName} | 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" |
| 103 | </span> | 105 | /> |
| 104 | </span> | 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 | <span aria-hidden className="text-muted-foreground/40"> | 117 | <span aria-hidden className="text-muted-foreground/40"> |
| 106 | / | 118 | / |
| 107 | </span> | 119 | </span> |