Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
reconcileRetainedMountedThreadIds,
resolveThreadMetadataUpdateForNextTurn,
resolveSendEnvMode,
startNewThreadForProject,
shouldShowBranchMismatchBanner,
shouldWriteThreadErrorToCurrentServerThread,
} from "./ChatView.logic";
Expand Down Expand Up @@ -445,6 +446,33 @@ describe("shouldWriteThreadErrorToCurrentServerThread", () => {
});
});

describe("startNewThreadForProject", () => {
it("starts a thread through the supplied shared handler for the active project", () => {
const calls: Array<{ environmentId: EnvironmentId; projectId: ProjectId }> = [];
const projectRef = { environmentId, projectId };

expect(
startNewThreadForProject(projectRef, (nextProjectRef) => {
calls.push(nextProjectRef);
return Promise.resolve();
}),
).toBe(true);
expect(calls).toEqual([projectRef]);
});

it("does nothing when the active project is unavailable", () => {
let called = false;

expect(
startNewThreadForProject(null, () => {
called = true;
return Promise.resolve();
}),
).toBe(false);
expect(called).toBe(false);
});
});

describe("hasServerAcknowledgedLocalDispatch", () => {
it("does not acknowledge unchanged server state", () => {
const localDispatch = createLocalDispatchSnapshot(
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ModelSelection,
type ProviderDriverKind,
type ServerProvider,
type ScopedProjectRef,
type ScopedThreadRef,
type ThreadId,
type TurnId,
Expand All @@ -27,6 +28,16 @@ export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3;

export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String);

export function startNewThreadForProject(
projectRef: ScopedProjectRef | null,
handleNewThread: (projectRef: ScopedProjectRef) => Promise<void>,
): boolean {
if (projectRef === null) return false;
void handleNewThread(projectRef);

return true;
}

export function resolveThreadMetadataUpdateForNextTurn(input: {
currentModelSelection: ModelSelection;
nextModelSelection?: ModelSelection;
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import { getProviderModelCapabilities, resolveSelectableProvider } from "../prov
import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";
import { useClientSettings, useEnvironmentSettings } from "../hooks/useSettings";
import { useNowMinute } from "../hooks/useNowMinute";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { resolveAppModelSelectionForInstance } from "../modelSelection";
import { getTerminalFocusOwner } from "../lib/terminalFocus";
import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions";
Expand Down Expand Up @@ -271,6 +272,7 @@ import {
resolveSendEnvMode,
revokeBlobPreviewUrl,
revokeUserMessagePreviewUrls,
startNewThreadForProject,
waitForStartedServerThread,
} from "./ChatView.logic";
import { useLocalStorage } from "~/hooks/useLocalStorage";
Expand Down Expand Up @@ -1130,6 +1132,7 @@ function ChatViewContent(props: ChatViewProps) {
forceExpandedMobileComposer = false,
} = props;
const draftId = routeKind === "draft" ? props.draftId : null;
const handleNewThread = useNewThreadHandler();
const routeThreadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
[environmentId, threadId],
Expand Down Expand Up @@ -1583,6 +1586,9 @@ function ChatViewContent(props: ChatViewProps) {
? scopeProjectRef(activeThread.environmentId, activeThread.projectId)
: null;
const activeProject = useProject(activeProjectRef);
const handleNewThreadInActiveProject = useCallback(() => {
startNewThreadForProject(activeProjectRef, handleNewThread);
}, [activeProjectRef, handleNewThread]);
const activeEnvironmentShell = useEnvironmentQuery(
activeThread ? environmentShell.stateAtom(activeThread.environmentId) : null,
);
Expand Down Expand Up @@ -5674,6 +5680,7 @@ function ChatViewContent(props: ChatViewProps) {
availableEditors={availableEditors}
rightPanelOpen={rightPanelOpen}
gitCwd={gitCwd}
onNewThreadInProject={handleNewThreadInActiveProject}
onRunProjectScript={runProjectScript}
onAddProjectScript={saveProjectScript}
onUpdateProjectScript={updateProjectScript}
Expand Down
32 changes: 22 additions & 10 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface ChatHeaderProps {
availableEditors: ReadonlyArray<EditorId>;
rightPanelOpen: boolean;
gitCwd: string | null;
onNewThreadInProject: () => void;
onRunProjectScript: (script: ProjectScript) => void;
onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>;
onUpdateProjectScript: (
Expand Down Expand Up @@ -69,6 +70,7 @@ export const ChatHeader = memo(function ChatHeader({
availableEditors,
rightPanelOpen,
gitCwd,
onNewThreadInProject,
onRunProjectScript,
onAddProjectScript,
onUpdateProjectScript,
Expand All @@ -92,16 +94,26 @@ export const ChatHeader = memo(function ChatHeader({
doesn't answer it. */}
{activeProjectName ? (
<span className="inline-flex shrink-0 items-center gap-2">
<span className="inline-flex min-w-0 items-center gap-1.5">
<ProjectFavicon
environmentId={activeThreadEnvironmentId}
cwd={activeProjectCwd ?? ""}
className="size-3.5"
/>
<span className="max-w-40 truncate text-sm font-medium text-muted-foreground">
{activeProjectName}
</span>
</span>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label={`New thread in ${activeProjectName}`}
onClick={onNewThreadInProject}
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"
/>
}
>
<ProjectFavicon
environmentId={activeThreadEnvironmentId}
cwd={activeProjectCwd ?? ""}
className="size-3.5"
/>
<span className="max-w-40 truncate text-sm font-medium">{activeProjectName}</span>
</TooltipTrigger>
<TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup>
</Tooltip>
<span aria-hidden className="text-muted-foreground/40">
/
</span>
Expand Down
Loading