Skip to content

Commit 8f78b0a

Browse files
committed
Handle non-git drafts in local mode
1 parent a70e8f3 commit 8f78b0a

4 files changed

Lines changed: 154 additions & 2 deletions

File tree

apps/web/src/components/BranchToolbar.logic.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ describe("resolveEffectiveEnvMode", () => {
138138
}),
139139
).toBe("worktree");
140140
});
141+
142+
it("falls back to local mode for non-git projects even if the draft prefers worktree mode", () => {
143+
expect(
144+
resolveEffectiveEnvMode({
145+
activeWorktreePath: null,
146+
hasServerThread: false,
147+
draftThreadEnvMode: "worktree",
148+
isGitRepo: false,
149+
}),
150+
).toBe("local");
151+
});
141152
});
142153

143154
describe("resolveEnvModeLabel", () => {

apps/web/src/components/BranchToolbar.logic.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ export function resolveEffectiveEnvMode(input: {
5454
activeWorktreePath: string | null;
5555
hasServerThread: boolean;
5656
draftThreadEnvMode: EnvMode | undefined;
57+
isGitRepo?: boolean;
5758
}): EnvMode {
58-
const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input;
59+
const { activeWorktreePath, hasServerThread, draftThreadEnvMode, isGitRepo = true } = input;
60+
if (!isGitRepo) {
61+
return "local";
62+
}
5963
if (!hasServerThread) {
6064
if (activeWorktreePath) {
6165
return "local";

apps/web/src/components/ChatView.browser.tsx

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,19 @@ import { BrowserWsRpcHarness, type NormalizedWsRpcRequestBody } from "../../test
4949
import { estimateTimelineMessageHeight } from "./timelineHeight";
5050
import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
5151

52+
const { gitStatusStateRef } = vi.hoisted(() => ({
53+
gitStatusStateRef: {
54+
current: { data: null, error: null, cause: null, isPending: false } as {
55+
data: unknown;
56+
error: unknown;
57+
cause: unknown;
58+
isPending: boolean;
59+
},
60+
},
61+
}));
62+
5263
vi.mock("../lib/gitStatusState", () => ({
53-
useGitStatus: () => ({ data: null, error: null, cause: null, isPending: false }),
64+
useGitStatus: () => gitStatusStateRef.current,
5465
useGitStatuses: () => new Map(),
5566
refreshGitStatus: () => Promise.resolve(null),
5667
resetGitStatusStateForTests: () => undefined,
@@ -1458,6 +1469,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
14581469
document.body.innerHTML = "";
14591470
wsRequests.length = 0;
14601471
customWsRpcResolver = null;
1472+
gitStatusStateRef.current = { data: null, error: null, cause: null, isPending: false };
14611473
useComposerDraftStore.setState({
14621474
draftsByThreadKey: {},
14631475
draftThreadsByThreadKey: {},
@@ -2448,6 +2460,100 @@ describe("ChatView timeline estimator parity (full app)", () => {
24482460
}
24492461
});
24502462

2463+
it("falls back to local send behavior for non-git drafts even when worktree mode is persisted", async () => {
2464+
gitStatusStateRef.current = {
2465+
data: {
2466+
isRepo: false,
2467+
hasOriginRemote: false,
2468+
isDefaultBranch: false,
2469+
branch: null,
2470+
hasWorkingTreeChanges: false,
2471+
workingTree: { files: [], insertions: 0, deletions: 0 },
2472+
hasUpstream: false,
2473+
aheadCount: 0,
2474+
behindCount: 0,
2475+
pr: null,
2476+
},
2477+
error: null,
2478+
cause: null,
2479+
isPending: false,
2480+
};
2481+
useComposerDraftStore.setState({
2482+
draftThreadsByThreadKey: {
2483+
[THREAD_KEY]: {
2484+
threadId: THREAD_ID,
2485+
environmentId: LOCAL_ENVIRONMENT_ID,
2486+
projectId: PROJECT_ID,
2487+
logicalProjectKey: PROJECT_DRAFT_KEY,
2488+
createdAt: NOW_ISO,
2489+
runtimeMode: "full-access",
2490+
interactionMode: "default",
2491+
branch: null,
2492+
worktreePath: null,
2493+
envMode: "worktree",
2494+
},
2495+
},
2496+
logicalProjectDraftThreadKeyByLogicalProjectKey: {
2497+
[PROJECT_DRAFT_KEY]: THREAD_KEY,
2498+
},
2499+
});
2500+
2501+
const mounted = await mountChatView({
2502+
viewport: DEFAULT_VIEWPORT,
2503+
snapshot: createDraftOnlySnapshot(),
2504+
resolveRpc: (body) => {
2505+
if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) {
2506+
return {
2507+
sequence: fixture.snapshot.snapshotSequence + 1,
2508+
};
2509+
}
2510+
return undefined;
2511+
},
2512+
});
2513+
2514+
try {
2515+
useComposerDraftStore.getState().setPrompt(THREAD_REF, "Ship it");
2516+
await waitForLayout();
2517+
2518+
const sendButton = await waitForSendButton();
2519+
expect(sendButton.disabled).toBe(false);
2520+
sendButton.click();
2521+
2522+
await vi.waitFor(
2523+
() => {
2524+
const dispatchRequest = wsRequests.find(
2525+
(request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand,
2526+
) as
2527+
| {
2528+
_tag: string;
2529+
bootstrap?: {
2530+
createThread?: { projectId?: string };
2531+
prepareWorktree?: unknown;
2532+
};
2533+
}
2534+
| undefined;
2535+
expect(dispatchRequest).toMatchObject({
2536+
_tag: ORCHESTRATION_WS_METHODS.dispatchCommand,
2537+
bootstrap: {
2538+
createThread: {
2539+
projectId: PROJECT_ID,
2540+
},
2541+
},
2542+
});
2543+
expect(dispatchRequest?.bootstrap?.prepareWorktree).toBeUndefined();
2544+
expect(document.querySelector('button[aria-label="Preparing worktree"]')).toBeNull();
2545+
},
2546+
{ timeout: 8_000, interval: 16 },
2547+
);
2548+
2549+
expect(useComposerDraftStore.getState().getDraftThread(THREAD_REF)?.envMode ?? null).toBe(
2550+
"local",
2551+
);
2552+
} finally {
2553+
await mounted.cleanup();
2554+
}
2555+
});
2556+
24512557
it("toggles plan mode with Shift+Tab only while the composer is focused", async () => {
24522558
const mounted = await mountChatView({
24532559
viewport: DEFAULT_VIEWPORT,

apps/web/src/components/ChatView.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,7 +2196,38 @@ export default function ChatView(props: ChatViewProps) {
21962196
activeWorktreePath,
21972197
hasServerThread: isServerThread,
21982198
draftThreadEnvMode: isLocalDraftThread ? draftThread?.envMode : undefined,
2199+
isGitRepo,
21992200
});
2201+
const draftThreadEnvMode = draftThread?.envMode;
2202+
const draftThreadBranch = draftThread?.branch;
2203+
2204+
useEffect(() => {
2205+
if (!isLocalDraftThread) {
2206+
return;
2207+
}
2208+
if (!draftThread) {
2209+
return;
2210+
}
2211+
if (gitStatusQuery.data?.isRepo !== false) {
2212+
return;
2213+
}
2214+
if (draftThreadEnvMode !== "worktree" && draftThreadBranch === null) {
2215+
return;
2216+
}
2217+
setDraftThreadContext(composerDraftTarget, {
2218+
envMode: "local",
2219+
branch: null,
2220+
worktreePath: null,
2221+
});
2222+
}, [
2223+
composerDraftTarget,
2224+
draftThread,
2225+
draftThreadBranch,
2226+
draftThreadEnvMode,
2227+
gitStatusQuery.data?.isRepo,
2228+
isLocalDraftThread,
2229+
setDraftThreadContext,
2230+
]);
22002231

22012232
useEffect(() => {
22022233
if (!activeThreadId) {

0 commit comments

Comments
 (0)