Skip to content

Commit 95cf6f0

Browse files
committed
fix: reduce icon size and add 'Default for project' checkbox
- Reduce icon size from 14px to 10px (default) to match RuntimeBadge - Reduce button padding from p-1.5 to p-1 - Add interactive tooltip with 'Default for project' checkbox - Separate current selection (non-persistent) from project default (persistent) - Clicking icon selects for this workspace, checkbox persists as default - Reuse existing 'runtime:{projectPath}' storage key for compatibility
1 parent e585296 commit 95cf6f0

File tree

8 files changed

+99
-27
lines changed

8 files changed

+99
-27
lines changed

src/browser/components/ChatInput/CreationControls.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ interface CreationControlsProps {
88
trunkBranch: string;
99
onTrunkBranchChange: (branch: string) => void;
1010
runtimeMode: RuntimeMode;
11+
defaultRuntimeMode: RuntimeMode;
1112
sshHost: string;
12-
/** Called when user changes runtime mode via checkbox in tooltip */
13+
/** Called when user clicks a runtime icon to select it (does not persist) */
1314
onRuntimeModeChange: (mode: RuntimeMode) => void;
15+
/** Called when user checks "Default for project" checkbox (persists) */
16+
onSetDefaultRuntime: (mode: RuntimeMode) => void;
1417
/** Called when user changes SSH host */
1518
onSshHostChange: (host: string) => void;
1619
disabled: boolean;
@@ -32,6 +35,8 @@ export function CreationControls(props: CreationControlsProps) {
3235
<RuntimeIconSelector
3336
value={props.runtimeMode}
3437
onChange={props.onRuntimeModeChange}
38+
defaultMode={props.defaultRuntimeMode}
39+
onSetDefault={props.onSetDefaultRuntime}
3540
disabled={props.disabled}
3641
/>
3742

src/browser/components/ChatInput/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
13821382
trunkBranch={creationState.trunkBranch}
13831383
onTrunkBranchChange={creationState.setTrunkBranch}
13841384
runtimeMode={creationState.runtimeMode}
1385+
defaultRuntimeMode={creationState.defaultRuntimeMode}
13851386
sshHost={creationState.sshHost}
13861387
onRuntimeModeChange={creationState.setRuntimeMode}
1388+
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
13871389
onSshHostChange={creationState.setSshHost}
13881390
disabled={creationState.isSending || isSending}
13891391
/>

src/browser/components/ChatInput/useCreationWorkspace.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,15 +473,18 @@ function createDraftSettingsHarness(
473473
sshHost: string;
474474
trunkBranch: string;
475475
runtimeString?: string | undefined;
476+
defaultRuntimeMode?: RuntimeMode;
476477
}>
477478
) {
478479
const state = {
479480
runtimeMode: initial?.runtimeMode ?? ("local" as RuntimeMode),
481+
defaultRuntimeMode: initial?.defaultRuntimeMode ?? ("worktree" as RuntimeMode),
480482
sshHost: initial?.sshHost ?? "",
481483
trunkBranch: initial?.trunkBranch ?? "main",
482484
runtimeString: initial?.runtimeString,
483485
} satisfies {
484486
runtimeMode: RuntimeMode;
487+
defaultRuntimeMode: RuntimeMode;
485488
sshHost: string;
486489
trunkBranch: string;
487490
runtimeString: string | undefined;
@@ -499,19 +502,28 @@ function createDraftSettingsHarness(
499502
state.runtimeString = mode === "ssh" ? (trimmedHost ? `ssh ${trimmedHost}` : "ssh") : undefined;
500503
});
501504

505+
const setDefaultRuntimeMode = mock((mode: RuntimeMode) => {
506+
state.defaultRuntimeMode = mode;
507+
state.runtimeMode = mode;
508+
const trimmedHost = state.sshHost.trim();
509+
state.runtimeString = mode === "ssh" ? (trimmedHost ? `ssh ${trimmedHost}` : "ssh") : undefined;
510+
});
511+
502512
const setSshHost = mock((host: string) => {
503513
state.sshHost = host;
504514
});
505515

506516
return {
507517
state,
508518
setRuntimeMode,
519+
setDefaultRuntimeMode,
509520
setSshHost,
510521
setTrunkBranch,
511522
getRuntimeString,
512523
snapshot(): {
513524
settings: DraftWorkspaceSettings;
514525
setRuntimeMode: typeof setRuntimeMode;
526+
setDefaultRuntimeMode: typeof setDefaultRuntimeMode;
515527
setSshHost: typeof setSshHost;
516528
setTrunkBranch: typeof setTrunkBranch;
517529
getRuntimeString: typeof getRuntimeString;
@@ -521,12 +533,14 @@ function createDraftSettingsHarness(
521533
thinkingLevel: "medium",
522534
mode: "exec",
523535
runtimeMode: state.runtimeMode,
536+
defaultRuntimeMode: state.defaultRuntimeMode,
524537
sshHost: state.sshHost,
525538
trunkBranch: state.trunkBranch,
526539
};
527540
return {
528541
settings,
529542
setRuntimeMode,
543+
setDefaultRuntimeMode,
530544
setSshHost,
531545
setTrunkBranch,
532546
getRuntimeString,

src/browser/components/ChatInput/useCreationWorkspace.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ interface UseCreationWorkspaceReturn {
4646
trunkBranch: string;
4747
setTrunkBranch: (branch: string) => void;
4848
runtimeMode: RuntimeMode;
49+
defaultRuntimeMode: RuntimeMode;
4950
sshHost: string;
50-
/** Set the default runtime mode for this project (only via checkbox) */
51+
/** Set the currently selected runtime mode (does not persist) */
5152
setRuntimeMode: (mode: RuntimeMode) => void;
53+
/** Set the default runtime mode for this project (persists via checkbox) */
54+
setDefaultRuntimeMode: (mode: RuntimeMode) => void;
5255
/** Set the SSH host (persisted separately from runtime mode) */
5356
setSshHost: (host: string) => void;
5457
toast: Toast | null;
@@ -75,8 +78,14 @@ export function useCreationWorkspace({
7578
const [isSending, setIsSending] = useState(false);
7679

7780
// Centralized draft workspace settings with automatic persistence
78-
const { settings, setRuntimeMode, setSshHost, setTrunkBranch, getRuntimeString } =
79-
useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);
81+
const {
82+
settings,
83+
setRuntimeMode,
84+
setDefaultRuntimeMode,
85+
setSshHost,
86+
setTrunkBranch,
87+
getRuntimeString,
88+
} = useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);
8089

8190
// Get send options from shared hook (uses project-scoped storage key)
8291
const sendMessageOptions = useSendMessageOptions(getProjectScopeId(projectPath));
@@ -183,8 +192,10 @@ export function useCreationWorkspace({
183192
trunkBranch: settings.trunkBranch,
184193
setTrunkBranch,
185194
runtimeMode: settings.runtimeMode,
195+
defaultRuntimeMode: settings.defaultRuntimeMode,
186196
sshHost: settings.sshHost,
187197
setRuntimeMode,
198+
setDefaultRuntimeMode,
188199
setSshHost,
189200
toast,
190201
setToast,

src/browser/components/RuntimeIconSelector.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { TooltipWrapper, Tooltip } from "./Tooltip";
77
interface RuntimeIconSelectorProps {
88
value: RuntimeMode;
99
onChange: (mode: RuntimeMode) => void;
10+
/** The persisted default runtime for this project */
11+
defaultMode: RuntimeMode;
12+
/** Called when user checks "Default for project" in tooltip */
13+
onSetDefault: (mode: RuntimeMode) => void;
1014
disabled?: boolean;
1115
className?: string;
1216
}
@@ -46,7 +50,9 @@ const RUNTIME_INFO: Record<RuntimeMode, { label: string; description: string }>
4650
interface RuntimeIconButtonProps {
4751
mode: RuntimeMode;
4852
isSelected: boolean;
53+
isDefault: boolean;
4954
onClick: () => void;
55+
onSetDefault: () => void;
5056
disabled?: boolean;
5157
}
5258

@@ -69,19 +75,28 @@ function RuntimeIconButton(props: RuntimeIconButtonProps) {
6975
onClick={props.onClick}
7076
disabled={props.disabled}
7177
className={cn(
72-
"inline-flex items-center justify-center rounded border p-1.5 transition-colors",
78+
"inline-flex items-center justify-center rounded border p-1 transition-colors",
7379
"focus:outline-none focus-visible:ring-1 focus-visible:ring-blue-500",
7480
stateStyle,
7581
props.disabled && "cursor-not-allowed opacity-50"
7682
)}
7783
aria-label={`${info.label} runtime`}
7884
aria-pressed={props.isSelected}
7985
>
80-
<Icon size={14} />
86+
<Icon />
8187
</button>
82-
<Tooltip align="center" width="wide" position="bottom">
88+
<Tooltip align="center" width="wide" position="bottom" interactive>
8389
<strong>{info.label}</strong>
84-
<p className="text-muted mt-0.5">{info.description}</p>
90+
<p className="text-muted mt-0.5 text-xs">{info.description}</p>
91+
<label className="mt-1.5 flex cursor-pointer items-center gap-1.5 text-xs">
92+
<input
93+
type="checkbox"
94+
checked={props.isDefault}
95+
onChange={() => props.onSetDefault()}
96+
className="accent-accent h-3 w-3"
97+
/>
98+
<span className="text-muted">Default for project</span>
99+
</label>
85100
</Tooltip>
86101
</TooltipWrapper>
87102
);
@@ -91,7 +106,7 @@ function RuntimeIconButton(props: RuntimeIconButtonProps) {
91106
* Runtime selector using icons with tooltips.
92107
* Shows Local, Worktree, and SSH options as clickable icons.
93108
* Selected runtime uses "active" styling (brighter colors).
94-
* Clicking an icon sets it as the project default.
109+
* Each tooltip has a "Default for project" checkbox to persist the preference.
95110
*/
96111
export function RuntimeIconSelector(props: RuntimeIconSelectorProps) {
97112
const modes: RuntimeMode[] = [RUNTIME_MODE.LOCAL, RUNTIME_MODE.WORKTREE, RUNTIME_MODE.SSH];
@@ -107,7 +122,9 @@ export function RuntimeIconSelector(props: RuntimeIconSelectorProps) {
107122
key={mode}
108123
mode={mode}
109124
isSelected={props.value === mode}
125+
isDefault={props.defaultMode === mode}
110126
onClick={() => props.onChange(mode)}
127+
onSetDefault={() => props.onSetDefault(mode)}
111128
disabled={props.disabled}
112129
/>
113130
))}

src/browser/hooks/useDraftWorkspaceSettings.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from "react";
1+
import { useState, useEffect } from "react";
22
import { usePersistedState } from "./usePersistedState";
33
import { useThinkingLevel } from "./useThinkingLevel";
44
import { useMode } from "@/browser/contexts/ModeContext";
@@ -10,7 +10,7 @@ import {
1010
} from "@/common/types/runtime";
1111
import {
1212
getModelKey,
13-
getDefaultRuntimeKey,
13+
getRuntimeKey,
1414
getTrunkBranchKey,
1515
getLastSshHostKey,
1616
getProjectScopeId,
@@ -29,7 +29,10 @@ export interface DraftWorkspaceSettings {
2929
mode: UIMode;
3030

3131
// Workspace creation settings (project-specific)
32+
/** Currently selected runtime for this workspace creation (may differ from default) */
3233
runtimeMode: RuntimeMode;
34+
/** Persisted default runtime for this project (used to initialize selection) */
35+
defaultRuntimeMode: RuntimeMode;
3336
sshHost: string;
3437
trunkBranch: string;
3538
}
@@ -49,7 +52,10 @@ export function useDraftWorkspaceSettings(
4952
recommendedTrunk: string | null
5053
): {
5154
settings: DraftWorkspaceSettings;
55+
/** Set the currently selected runtime mode (does not persist) */
5256
setRuntimeMode: (mode: RuntimeMode) => void;
57+
/** Set the default runtime mode for this project (persists via checkbox) */
58+
setDefaultRuntimeMode: (mode: RuntimeMode) => void;
5359
setSshHost: (host: string) => void;
5460
setTrunkBranch: (branch: string) => void;
5561
getRuntimeString: () => string | undefined;
@@ -68,11 +74,23 @@ export function useDraftWorkspaceSettings(
6874

6975
// Project-scoped default runtime (worktree by default, only changed via checkbox)
7076
const [defaultRuntimeString, setDefaultRuntimeString] = usePersistedState<string | undefined>(
71-
getDefaultRuntimeKey(projectPath),
77+
getRuntimeKey(projectPath),
7278
undefined, // undefined means worktree (the app default)
7379
{ listener: true }
7480
);
7581

82+
// Parse default runtime string into mode (worktree when undefined)
83+
const { mode: defaultRuntimeMode } = parseRuntimeModeAndHost(defaultRuntimeString);
84+
85+
// Currently selected runtime mode for this session (initialized from default)
86+
// This allows user to select a different runtime without changing the default
87+
const [selectedRuntimeMode, setSelectedRuntimeMode] = useState<RuntimeMode>(defaultRuntimeMode);
88+
89+
// Sync selected mode when default changes (e.g., from checkbox or project switch)
90+
useEffect(() => {
91+
setSelectedRuntimeMode(defaultRuntimeMode);
92+
}, [defaultRuntimeMode]);
93+
7694
// Project-scoped trunk branch preference (persisted per project)
7795
const [trunkBranch, setTrunkBranch] = usePersistedState<string>(
7896
getTrunkBranchKey(projectPath),
@@ -88,10 +106,6 @@ export function useDraftWorkspaceSettings(
88106
{ listener: true }
89107
);
90108

91-
// Parse default runtime string into mode (worktree when undefined)
92-
// SSH host is stored separately so it persists across mode switches
93-
const { mode: runtimeMode } = parseRuntimeModeAndHost(defaultRuntimeString);
94-
95109
// Initialize trunk branch from backend recommendation or first branch
96110
useEffect(() => {
97111
if (!trunkBranch && branches.length > 0) {
@@ -100,32 +114,41 @@ export function useDraftWorkspaceSettings(
100114
}
101115
}, [branches, recommendedTrunk, trunkBranch, setTrunkBranch]);
102116

103-
// Setter for default runtime mode (only way to change is via checkbox)
117+
// Setter for selected runtime mode (changes current selection, does not persist)
104118
const setRuntimeMode = (newMode: RuntimeMode) => {
119+
setSelectedRuntimeMode(newMode);
120+
};
121+
122+
// Setter for default runtime mode (persists via checkbox in tooltip)
123+
const setDefaultRuntimeMode = (newMode: RuntimeMode) => {
105124
const newRuntimeString = buildRuntimeString(newMode, lastSshHost);
106125
setDefaultRuntimeString(newRuntimeString);
126+
// Also update selection to match new default
127+
setSelectedRuntimeMode(newMode);
107128
};
108129

109130
// Setter for SSH host (persisted separately so it's remembered across mode switches)
110131
const setSshHost = (newHost: string) => {
111132
setLastSshHost(newHost);
112133
};
113134

114-
// Helper to get runtime string for IPC calls
135+
// Helper to get runtime string for IPC calls (uses selected mode, not default)
115136
const getRuntimeString = (): string | undefined => {
116-
return buildRuntimeString(runtimeMode, lastSshHost);
137+
return buildRuntimeString(selectedRuntimeMode, lastSshHost);
117138
};
118139

119140
return {
120141
settings: {
121142
model,
122143
thinkingLevel,
123144
mode,
124-
runtimeMode,
145+
runtimeMode: selectedRuntimeMode,
146+
defaultRuntimeMode,
125147
sshHost: lastSshHost,
126148
trunkBranch,
127149
},
128150
setRuntimeMode,
151+
setDefaultRuntimeMode,
129152
setSshHost,
130153
setTrunkBranch,
131154
getRuntimeString,

src/browser/utils/chatCommands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { applyCompactionOverrides } from "@/browser/utils/messages/compactionOpt
2424
import { resolveCompactionModel } from "@/browser/utils/messages/compactionModelPreference";
2525
import type { ImageAttachment } from "../components/ImageAttachments";
2626
import { dispatchWorkspaceSwitch } from "./workspaceEvents";
27-
import { getDefaultRuntimeKey, copyWorkspaceStorage } from "@/common/constants/storage";
27+
import { getRuntimeKey, copyWorkspaceStorage } from "@/common/constants/storage";
2828
import { DEFAULT_COMPACTION_WORD_TARGET, WORDS_TO_TOKENS_RATIO } from "@/common/constants/ui";
2929

3030
// ============================================================================
@@ -502,8 +502,8 @@ export async function createNewWorkspace(
502502
// Use saved default runtime preference if not explicitly provided
503503
let effectiveRuntime = options.runtime;
504504
if (effectiveRuntime === undefined) {
505-
const defaultRuntimeKey = getDefaultRuntimeKey(options.projectPath);
506-
const savedRuntime = localStorage.getItem(defaultRuntimeKey);
505+
const runtimeKey = getRuntimeKey(options.projectPath);
506+
const savedRuntime = localStorage.getItem(runtimeKey);
507507
if (savedRuntime) {
508508
effectiveRuntime = savedRuntime;
509509
}

src/common/constants/storage.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ export function getModeKey(workspaceId: string): string {
110110

111111
/**
112112
* Get the localStorage key for the default runtime for a project
113-
* Defaults to worktree if not set; can only be changed via the runtime icon selector.
114-
* Format: "defaultRuntime:{projectPath}"
113+
* Defaults to worktree if not set; can only be changed via the "Default for project" checkbox.
114+
* Format: "runtime:{projectPath}"
115115
*/
116-
export function getDefaultRuntimeKey(projectPath: string): string {
117-
return `defaultRuntime:${projectPath}`;
116+
export function getRuntimeKey(projectPath: string): string {
117+
return `runtime:${projectPath}`;
118118
}
119119

120120
/**

0 commit comments

Comments
 (0)