Skip to content

Commit e3f0355

Browse files
authored
Merge branch 'main' into feat/default-thinking-level-setting
2 parents 70aeacf + 89ffcf4 commit e3f0355

20 files changed

Lines changed: 524 additions & 269 deletions

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/marketing/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
"typecheck": "astro check"
1111
},
1212
"dependencies": {
13-
"astro": "^5.7.13"
13+
"astro": "^6.0.4"
1414
},
1515
"devDependencies": {
16-
"@astrojs/check": "^0.9.4",
16+
"@astrojs/check": "^0.9.7",
1717
"typescript": "catalog:"
1818
}
1919
}

apps/web/src/appSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const AppSettingsSchema = Schema.Struct({
3333
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
3434
Schema.withConstructorDefault(() => Option.some("")),
3535
),
36+
defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe(
37+
Schema.withConstructorDefault(() => Option.some("local")),
38+
),
3639
confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),
3740
enableAssistantStreaming: Schema.Boolean.pipe(
3841
Schema.withConstructorDefault(() => Option.some(false)),

apps/web/src/components/ChatView.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,17 @@ const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;
175175
const SCRIPT_TERMINAL_COLS = 120;
176176
const SCRIPT_TERMINAL_ROWS = 30;
177177

178+
const extendReplacementRangeForTrailingSpace = (
179+
text: string,
180+
rangeEnd: number,
181+
replacement: string,
182+
): number => {
183+
if (!replacement.endsWith(" ")) {
184+
return rangeEnd;
185+
}
186+
return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd;
187+
};
188+
178189
interface ChatViewProps {
179190
threadId: ThreadId;
180191
}
@@ -3017,17 +3028,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
30173028
};
30183029
}, [composerCursor]);
30193030

3020-
const extendReplacementRangeForTrailingSpace = (
3021-
text: string,
3022-
rangeEnd: number,
3023-
replacement: string,
3024-
): number => {
3025-
if (!replacement.endsWith(" ")) {
3026-
return rangeEnd;
3027-
}
3028-
return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd;
3029-
};
3030-
30313031
const resolveActiveComposerTrigger = useCallback((): {
30323032
snapshot: { value: string; cursor: number; expandedCursor: number };
30333033
trigger: ComposerTrigger | null;

apps/web/src/components/DiffPanel.tsx

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import { cn } from "~/lib/utils";
1919
import { readNativeApi } from "../nativeApi";
2020
import { resolvePathLinkTarget } from "../terminal-links";
2121
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
22-
import { isElectron } from "../env";
2322
import { useTheme } from "../hooks/useTheme";
2423
import { buildPatchCacheKey } from "../lib/diffRendering";
2524
import { resolveDiffThemeName } from "../lib/diffRendering";
2625
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
2726
import { useStore } from "../store";
2827
import { useAppSettings } from "../appSettings";
2928
import { formatShortTimestamp } from "../timestampFormat";
29+
import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
3030
import { ToggleGroup, Toggle } from "./ui/toggle-group";
3131

3232
type DiffRenderMode = "stacked" | "split";
@@ -152,7 +152,7 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
152152
}
153153

154154
interface DiffPanelProps {
155-
mode?: "inline" | "sheet" | "sidebar";
155+
mode?: DiffPanelMode;
156156
}
157157

158158
export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
@@ -398,7 +398,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
398398
selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
399399
}, [selectedTurn?.turnId, selectedTurnId]);
400400

401-
const shouldUseDragRegion = isElectron && mode !== "sheet";
402401
const headerRow = (
403402
<>
404403
<div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]">
@@ -512,28 +511,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
512511
</ToggleGroup>
513512
</>
514513
);
515-
const headerRowClassName = cn(
516-
"flex items-center justify-between gap-2 px-4",
517-
shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
518-
);
519514

520515
return (
521-
<div
522-
className={cn(
523-
"flex h-full min-w-0 flex-col bg-background",
524-
mode === "inline"
525-
? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
526-
: "w-full",
527-
)}
528-
>
529-
{shouldUseDragRegion ? (
530-
<div className={headerRowClassName}>{headerRow}</div>
531-
) : (
532-
<div className="border-b border-border">
533-
<div className={headerRowClassName}>{headerRow}</div>
534-
</div>
535-
)}
536-
516+
<DiffPanelShell mode={mode} header={headerRow}>
537517
{!activeThread ? (
538518
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
539519
Select a thread to inspect turn diffs.
@@ -558,15 +538,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
558538
</div>
559539
)}
560540
{!renderablePatch ? (
561-
<div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
562-
<p>
563-
{isLoadingCheckpointDiff
564-
? "Loading checkpoint diff..."
565-
: hasNoNetChanges
541+
isLoadingCheckpointDiff ? (
542+
<DiffPanelLoadingState label="Loading checkpoint diff..." />
543+
) : (
544+
<div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
545+
<p>
546+
{hasNoNetChanges
566547
? "No net changes in this selection."
567548
: "No patch available for this selection."}
568-
</p>
569-
</div>
549+
</p>
550+
</div>
551+
)
570552
) : renderablePatch.kind === "files" ? (
571553
<Virtualizer
572554
className="diff-render-surface h-full min-h-0 overflow-auto px-2 pb-2"
@@ -622,6 +604,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
622604
</div>
623605
</>
624606
)}
625-
</div>
607+
</DiffPanelShell>
626608
);
627609
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { ReactNode } from "react";
2+
3+
import { isElectron } from "~/env";
4+
import { cn } from "~/lib/utils";
5+
6+
import { Skeleton } from "./ui/skeleton";
7+
8+
export type DiffPanelMode = "inline" | "sheet" | "sidebar";
9+
10+
function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
11+
const shouldUseDragRegion = isElectron && mode !== "sheet";
12+
return cn(
13+
"flex items-center justify-between gap-2 px-4",
14+
shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
15+
);
16+
}
17+
18+
export function DiffPanelShell(props: {
19+
mode: DiffPanelMode;
20+
header: ReactNode;
21+
children: ReactNode;
22+
}) {
23+
const shouldUseDragRegion = isElectron && props.mode !== "sheet";
24+
25+
return (
26+
<div
27+
className={cn(
28+
"flex h-full min-w-0 flex-col bg-background",
29+
props.mode === "inline"
30+
? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
31+
: "w-full",
32+
)}
33+
>
34+
{shouldUseDragRegion ? (
35+
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
36+
) : (
37+
<div className="border-b border-border">
38+
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
39+
</div>
40+
)}
41+
{props.children}
42+
</div>
43+
);
44+
}
45+
46+
export function DiffPanelHeaderSkeleton() {
47+
return (
48+
<>
49+
<div className="relative min-w-0 flex-1">
50+
<Skeleton className="absolute left-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
51+
<Skeleton className="absolute right-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
52+
<div className="flex gap-1 overflow-hidden px-8 py-0.5">
53+
<Skeleton className="h-6 w-16 shrink-0 rounded-md" />
54+
<Skeleton className="h-6 w-24 shrink-0 rounded-md" />
55+
<Skeleton className="h-6 w-24 shrink-0 rounded-md max-sm:hidden" />
56+
</div>
57+
</div>
58+
<div className="flex shrink-0 gap-1">
59+
<Skeleton className="size-7 rounded-md" />
60+
<Skeleton className="size-7 rounded-md" />
61+
</div>
62+
</>
63+
);
64+
}
65+
66+
export function DiffPanelLoadingState(props: { label: string }) {
67+
return (
68+
<div className="flex min-h-0 flex-1 flex-col p-2">
69+
<div
70+
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border/60 bg-card/25"
71+
role="status"
72+
aria-live="polite"
73+
aria-label={props.label}
74+
>
75+
<div className="flex items-center gap-2 border-b border-border/50 px-3 py-2">
76+
<Skeleton className="h-4 w-32 rounded-full" />
77+
<Skeleton className="ml-auto h-4 w-20 rounded-full" />
78+
</div>
79+
<div className="flex min-h-0 flex-1 flex-col gap-4 px-3 py-4">
80+
<div className="space-y-2">
81+
<Skeleton className="h-3 w-full rounded-full" />
82+
<Skeleton className="h-3 w-full rounded-full" />
83+
<Skeleton className="h-3 w-10/12 rounded-full" />
84+
<Skeleton className="h-3 w-11/12 rounded-full" />
85+
<Skeleton className="h-3 w-9/12 rounded-full" />
86+
</div>
87+
<span className="sr-only">{props.label}</span>
88+
</div>
89+
</div>
90+
</div>
91+
);
92+
}

apps/web/src/components/PlanSidebar.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useState, useCallback, useRef, useEffect } from "react";
1+
import { memo, useState, useCallback } from "react";
22
import { type TimestampFormat } from "../appSettings";
33
import { Badge } from "./ui/badge";
44
import { Button } from "./ui/button";
@@ -26,6 +26,7 @@ import {
2626
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu";
2727
import { readNativeApi } from "~/nativeApi";
2828
import { toastManager } from "./ui/toast";
29+
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
2930

3031
function stepStatusIcon(status: string): React.ReactNode {
3132
if (status === "completed") {
@@ -68,35 +69,16 @@ const PlanSidebar = memo(function PlanSidebar({
6869
}: PlanSidebarProps) {
6970
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
7071
const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false);
71-
const [copied, setCopied] = useState(false);
72-
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
72+
const { copyToClipboard, isCopied } = useCopyToClipboard();
7373

7474
const planMarkdown = activeProposedPlan?.planMarkdown ?? null;
7575
const displayedPlanMarkdown = planMarkdown ? stripDisplayedPlanMarkdown(planMarkdown) : null;
7676
const planTitle = planMarkdown ? proposedPlanTitle(planMarkdown) : null;
7777

7878
const handleCopyPlan = useCallback(() => {
7979
if (!planMarkdown) return;
80-
void navigator.clipboard.writeText(planMarkdown);
81-
if (copiedTimerRef.current != null) {
82-
clearTimeout(copiedTimerRef.current);
83-
}
84-
85-
setCopied(true);
86-
copiedTimerRef.current = setTimeout(() => {
87-
setCopied(false);
88-
copiedTimerRef.current = null;
89-
}, 2000);
90-
}, [planMarkdown]);
91-
92-
// Cleanup timeout on unmount
93-
useEffect(() => {
94-
return () => {
95-
if (copiedTimerRef.current != null) {
96-
clearTimeout(copiedTimerRef.current);
97-
}
98-
};
99-
}, []);
80+
copyToClipboard(planMarkdown);
81+
}, [planMarkdown, copyToClipboard]);
10082

10183
const handleDownload = useCallback(() => {
10284
if (!planMarkdown) return;
@@ -169,7 +151,7 @@ const PlanSidebar = memo(function PlanSidebar({
169151
</MenuTrigger>
170152
<MenuPopup align="end">
171153
<MenuItem onClick={handleCopyPlan}>
172-
{copied ? "Copied!" : "Copy to clipboard"}
154+
{isCopied ? "Copied!" : "Copy to clipboard"}
173155
</MenuItem>
174156
<MenuItem onClick={handleDownload}>Download as markdown</MenuItem>
175157
<MenuItem

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,
@@ -63,6 +64,25 @@ describe("shouldClearThreadSelectionOnMouseDown", () => {
6364
});
6465
});
6566

67+
describe("resolveSidebarNewThreadEnvMode", () => {
68+
it("uses the app default when the caller does not request a specific mode", () => {
69+
expect(
70+
resolveSidebarNewThreadEnvMode({
71+
defaultEnvMode: "worktree",
72+
}),
73+
).toBe("worktree");
74+
});
75+
76+
it("preserves an explicit requested mode over the app default", () => {
77+
expect(
78+
resolveSidebarNewThreadEnvMode({
79+
requestedEnvMode: "local",
80+
defaultEnvMode: "worktree",
81+
}),
82+
).toBe("local");
83+
});
84+
});
85+
6686
describe("resolveThreadStatusPill", () => {
6787
const baseThread = {
6888
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:
@@ -38,6 +39,13 @@ export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null
3839
return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);
3940
}
4041

42+
export function resolveSidebarNewThreadEnvMode(input: {
43+
requestedEnvMode?: SidebarNewThreadEnvMode;
44+
defaultEnvMode: SidebarNewThreadEnvMode;
45+
}): SidebarNewThreadEnvMode {
46+
return input.requestedEnvMode ?? input.defaultEnvMode;
47+
}
48+
4149
export function resolveThreadRowClassName(input: {
4250
isActive: boolean;
4351
isSelected: boolean;

0 commit comments

Comments
 (0)