Skip to content

Commit f7e81fa

Browse files
binbanditjuliusmarminge
authored andcommitted
fix(web): add default thread env mode setting (pingdotgg#892)
Co-authored-by: Julius Marminge <julius0216@outlook.com>
1 parent 60c9541 commit f7e81fa

8 files changed

Lines changed: 95 additions & 5 deletions

File tree

KEYBINDINGS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged
5151
- `terminal.new`: create new terminal (in focused terminal context by default)
5252
- `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
5353
- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
54-
- `chat.newLocal`: create a new local chat thread for the active project (no worktree context)
54+
- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
5555
- `editor.openFavorite`: open current project/worktree in the last-used editor
5656
- `script.{id}.run`: run a project script by id (for example `script.test.run`)
5757

apps/web/src/appSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const AppSettingsSchema = Schema.Struct({
2323
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
2424
Schema.withConstructorDefault(() => Option.some("")),
2525
),
26+
defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe(
27+
Schema.withConstructorDefault(() => Option.some("local")),
28+
),
2629
confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),
2730
enableAssistantStreaming: Schema.Boolean.pipe(
2831
Schema.withConstructorDefault(() => Option.some(false)),

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22

33
import {
44
hasUnseenCompletion,
5+
resolveSidebarNewThreadEnvMode,
56
resolveThreadRowClassName,
67
resolveThreadStatusPill,
78
shouldClearThreadSelectionOnMouseDown,
@@ -64,6 +65,25 @@ describe("shouldClearThreadSelectionOnMouseDown", () => {
6465
});
6566
});
6667

68+
describe("resolveSidebarNewThreadEnvMode", () => {
69+
it("uses the app default when the caller does not request a specific mode", () => {
70+
expect(
71+
resolveSidebarNewThreadEnvMode({
72+
defaultEnvMode: "worktree",
73+
}),
74+
).toBe("worktree");
75+
});
76+
77+
it("preserves an explicit requested mode over the app default", () => {
78+
expect(
79+
resolveSidebarNewThreadEnvMode({
80+
requestedEnvMode: "local",
81+
defaultEnvMode: "worktree",
82+
}),
83+
).toBe("local");
84+
});
85+
});
86+
6787
describe("resolveThreadStatusPill", () => {
6888
const baseThread = {
6989
interactionMode: "plan" as const,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { cn } from "../lib/utils";
33
import { findLatestProposedPlan, isLatestTurnSettled } from "../session-logic";
44

55
export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";
6+
export type SidebarNewThreadEnvMode = "local" | "worktree";
67

78
export interface ThreadStatusPill {
89
label:
@@ -44,6 +45,13 @@ export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null
4445
return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);
4546
}
4647

48+
export function resolveSidebarNewThreadEnvMode(input: {
49+
requestedEnvMode?: SidebarNewThreadEnvMode;
50+
defaultEnvMode: SidebarNewThreadEnvMode;
51+
}): SidebarNewThreadEnvMode {
52+
return input.requestedEnvMode ?? input.defaultEnvMode;
53+
}
54+
4755
export function resolveThreadRowClassName(input: {
4856
isActive: boolean;
4957
isSelected: boolean;

apps/web/src/components/Sidebar.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { useThreadSelectionStore } from "../threadSelectionStore";
8585
import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
8686
import { isNonEmpty as isNonEmptyString } from "effect/String";
8787
import {
88+
resolveSidebarNewThreadEnvMode,
8889
resolveThreadRowClassName,
8990
resolveThreadStatusPill,
9091
shouldClearThreadSelectionOnMouseDown,
@@ -435,7 +436,9 @@ export default function Sidebar() {
435436
defaultModel: DEFAULT_MODEL_BY_PROVIDER.codex,
436437
createdAt,
437438
});
438-
await handleNewThread(projectId).catch(() => undefined);
439+
await handleNewThread(projectId, {
440+
envMode: appSettings.defaultThreadEnvMode,
441+
}).catch(() => undefined);
439442
} catch (error) {
440443
const description =
441444
error instanceof Error ? error.message : "An error occurred while adding the project.";
@@ -459,6 +462,7 @@ export default function Sidebar() {
459462
isAddingProject,
460463
projects,
461464
shouldBrowseForProjectImmediately,
465+
appSettings.defaultThreadEnvMode,
462466
],
463467
);
464468

@@ -1353,7 +1357,11 @@ export default function Sidebar() {
13531357
onClick={(event) => {
13541358
event.preventDefault();
13551359
event.stopPropagation();
1356-
void handleNewThread(project.id);
1360+
void handleNewThread(project.id, {
1361+
envMode: resolveSidebarNewThreadEnvMode({
1362+
defaultEnvMode: appSettings.defaultThreadEnvMode,
1363+
}),
1364+
});
13571365
}}
13581366
>
13591367
<SquarePenIcon className="size-3.5" />

apps/web/src/components/ui/switch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
88
return (
99
<SwitchPrimitive.Root
1010
className={cn(
11-
"inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]",
11+
"inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 cursor-pointer items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:cursor-not-allowed data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]",
1212
className,
1313
)}
1414
data-slot="switch"

apps/web/src/routes/_chat.settings.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,49 @@ function SettingsRouteView() {
523523
</div>
524524
</section>
525525

526+
<section className="rounded-2xl border border-border bg-card p-5">
527+
<div className="mb-4">
528+
<h2 className="text-sm font-medium text-foreground">Threads</h2>
529+
<p className="mt-1 text-xs text-muted-foreground">
530+
Choose the default workspace mode for newly created draft threads.
531+
</p>
532+
</div>
533+
534+
<div className="flex items-center justify-between rounded-lg border border-border bg-background px-3 py-2">
535+
<div>
536+
<p className="text-sm font-medium text-foreground">Default to New worktree</p>
537+
<p className="text-xs text-muted-foreground">
538+
New threads start in New worktree mode instead of Local.
539+
</p>
540+
</div>
541+
<Switch
542+
checked={settings.defaultThreadEnvMode === "worktree"}
543+
onCheckedChange={(checked) =>
544+
updateSettings({
545+
defaultThreadEnvMode: checked ? "worktree" : "local",
546+
})
547+
}
548+
aria-label="Default new threads to New worktree mode"
549+
/>
550+
</div>
551+
552+
{settings.defaultThreadEnvMode !== defaults.defaultThreadEnvMode ? (
553+
<div className="mt-3 flex justify-end">
554+
<Button
555+
size="xs"
556+
variant="outline"
557+
onClick={() =>
558+
updateSettings({
559+
defaultThreadEnvMode: defaults.defaultThreadEnvMode,
560+
})
561+
}
562+
>
563+
Restore default
564+
</Button>
565+
</div>
566+
) : null}
567+
</section>
568+
526569
<section className="rounded-2xl border border-border bg-card p-5">
527570
<div className="mb-4">
528571
<h2 className="text-sm font-medium text-foreground">Responses</h2>

apps/web/src/routes/_chat.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { resolveShortcutCommand } from "../keybindings";
1212
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
1313
import { useThreadSelectionStore } from "../threadSelectionStore";
1414
import { Sidebar, SidebarProvider } from "~/components/ui/sidebar";
15+
import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic";
16+
import { useAppSettings } from "~/appSettings";
1517

1618
const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];
1719

@@ -27,6 +29,7 @@ function ChatRouteGlobalShortcuts() {
2729
? selectThreadTerminalState(state.terminalStateByThreadId, routeThreadId).terminalOpen
2830
: false,
2931
);
32+
const { settings: appSettings } = useAppSettings();
3033

3134
useEffect(() => {
3235
const onWindowKeyDown = (event: KeyboardEvent) => {
@@ -51,7 +54,11 @@ function ChatRouteGlobalShortcuts() {
5154
if (command === "chat.newLocal") {
5255
event.preventDefault();
5356
event.stopPropagation();
54-
void handleNewThread(projectId);
57+
void handleNewThread(projectId, {
58+
envMode: resolveSidebarNewThreadEnvMode({
59+
defaultEnvMode: appSettings.defaultThreadEnvMode,
60+
}),
61+
});
5562
return;
5663
}
5764

@@ -80,6 +87,7 @@ function ChatRouteGlobalShortcuts() {
8087
projects,
8188
selectedThreadIdsSize,
8289
terminalOpen,
90+
appSettings.defaultThreadEnvMode,
8391
]);
8492

8593
return null;

0 commit comments

Comments
 (0)