Add Claude Code settings infrastructure, TUI mode, session naming, and global defaults
Adds first-class support for Claude Code CLI features (2.1.71-2.1.110): - New ClaudeCodeSettings struct with per-project and global defaults for TUI mode, effort level, focus mode, thinking summaries, session recap, auto-scroll, env scrub, and 1-hour prompt caching - Settings injected as env vars (CLAUDE_CODE_NO_FLICKER, etc.) and ~/.claude/settings.json entries via entrypoint.sh merge block - New ClaudeCodeSettingsModal component for configuring settings - Session naming support (-n flag passed to claude CLI, shown in tabs) - Relaxed reserved prefix filter: CLAUDE_CODE_* env vars now allowed in custom env vars UI for power users - Global SSH key path, git name, and git email now used as fallbacks when per-project values are not set, with UI in SettingsPanel - Fingerprint-based change detection triggers container recreation when Claude Code settings change - Updated README, HOW-TO-USE, and CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
app/src/components/projects/ClaudeCodeSettingsModal.tsx
Normal file
191
app/src/components/projects/ClaudeCodeSettingsModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import type { ClaudeCodeSettings } from "../../lib/types";
|
||||
|
||||
interface Props {
|
||||
settings: ClaudeCodeSettings | null;
|
||||
disabled: boolean;
|
||||
onSave: (settings: ClaudeCodeSettings | null) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DEFAULTS: ClaudeCodeSettings = {
|
||||
tui_mode: null,
|
||||
effort: null,
|
||||
auto_scroll_disabled: false,
|
||||
focus_mode: false,
|
||||
show_thinking_summaries: false,
|
||||
enable_session_recap: false,
|
||||
env_scrub: false,
|
||||
prompt_caching_1h: false,
|
||||
};
|
||||
|
||||
function isAllDefaults(s: ClaudeCodeSettings): boolean {
|
||||
return (
|
||||
s.tui_mode === null &&
|
||||
s.effort === null &&
|
||||
s.auto_scroll_disabled === false &&
|
||||
s.focus_mode === false &&
|
||||
s.show_thinking_summaries === false &&
|
||||
s.enable_session_recap === false &&
|
||||
s.env_scrub === false &&
|
||||
s.prompt_caching_1h === false
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClaudeCodeSettingsModal({ settings, disabled, onSave, onClose }: Props) {
|
||||
const [local, setLocal] = useState<ClaudeCodeSettings>(settings ?? { ...DEFAULTS });
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === overlayRef.current) onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const update = async (patch: Partial<ClaudeCodeSettings>) => {
|
||||
const next = { ...local, ...patch };
|
||||
setLocal(next);
|
||||
try {
|
||||
await onSave(isAllDefaults(next) ? null : next);
|
||||
} catch (err) {
|
||||
console.error("Failed to save Claude Code settings:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleButton = (label: string, description: string, value: boolean, onChange: (v: boolean) => void) => (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{label}</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">{description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onChange(!value)}
|
||||
disabled={disabled}
|
||||
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 shrink-0 ${
|
||||
value
|
||||
? "bg-[var(--success)] text-white"
|
||||
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
{value ? "ON" : "OFF"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
onClick={handleOverlayClick}
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-[32rem] shadow-xl max-h-[80vh] overflow-y-auto">
|
||||
<h2 className="text-lg font-semibold mb-4">Claude Code Settings</h2>
|
||||
|
||||
{disabled && (
|
||||
<div className="px-2 py-1.5 mb-3 bg-[var(--warning)]/15 border border-[var(--warning)]/30 rounded text-xs text-[var(--warning)]">
|
||||
Container must be stopped to change Claude Code settings.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
{/* TUI Mode */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">TUI Mode</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">Enables flicker-free alt-screen rendering</div>
|
||||
</div>
|
||||
<select
|
||||
value={local.tui_mode ?? ""}
|
||||
onChange={(e) => update({ tui_mode: e.target.value || null })}
|
||||
disabled={disabled}
|
||||
className="px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 shrink-0"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
<option value="fullscreen">Fullscreen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Effort Level */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">Effort Level</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">Controls how much reasoning Claude applies</div>
|
||||
</div>
|
||||
<select
|
||||
value={local.effort ?? ""}
|
||||
onChange={(e) => update({ effort: e.target.value || null })}
|
||||
disabled={disabled}
|
||||
className="px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 shrink-0"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Boolean toggles */}
|
||||
{toggleButton(
|
||||
"Focus Mode",
|
||||
"Collapses tool output to one-line summaries",
|
||||
local.focus_mode,
|
||||
(v) => update({ focus_mode: v }),
|
||||
)}
|
||||
|
||||
{toggleButton(
|
||||
"Thinking Summaries",
|
||||
"Shows thinking process as summaries",
|
||||
local.show_thinking_summaries,
|
||||
(v) => update({ show_thinking_summaries: v }),
|
||||
)}
|
||||
|
||||
{toggleButton(
|
||||
"Session Recap",
|
||||
"Provides context when returning to a session",
|
||||
local.enable_session_recap,
|
||||
(v) => update({ enable_session_recap: v }),
|
||||
)}
|
||||
|
||||
{toggleButton(
|
||||
"Auto-Scroll Disabled",
|
||||
"Disables auto-scroll when in fullscreen TUI mode",
|
||||
local.auto_scroll_disabled,
|
||||
(v) => update({ auto_scroll_disabled: v }),
|
||||
)}
|
||||
|
||||
{toggleButton(
|
||||
"Env Scrub",
|
||||
"Strips credentials from subprocess environments for security",
|
||||
local.env_scrub,
|
||||
(v) => update({ env_scrub: v }),
|
||||
)}
|
||||
|
||||
{toggleButton(
|
||||
"Prompt Caching (1h)",
|
||||
"Enables 1-hour prompt cache TTL instead of 5 minutes",
|
||||
local.prompt_caching_1h,
|
||||
(v) => update({ prompt_caching_1h: v }),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useAppState } from "../../store/appState";
|
||||
import EnvVarsModal from "./EnvVarsModal";
|
||||
import PortMappingsModal from "./PortMappingsModal";
|
||||
import ClaudeInstructionsModal from "./ClaudeInstructionsModal";
|
||||
import ClaudeCodeSettingsModal from "./ClaudeCodeSettingsModal";
|
||||
import ContainerProgressModal from "./ContainerProgressModal";
|
||||
import FileManagerModal from "./FileManagerModal";
|
||||
import ConfirmRemoveModal from "./ConfirmRemoveModal";
|
||||
@@ -30,6 +31,7 @@ export default function ProjectCard({ project }: Props) {
|
||||
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
||||
const [showPortMappingsModal, setShowPortMappingsModal] = useState(false);
|
||||
const [showClaudeInstructionsModal, setShowClaudeInstructionsModal] = useState(false);
|
||||
const [showClaudeCodeSettingsModal, setShowClaudeCodeSettingsModal] = useState(false);
|
||||
const [showFileManager, setShowFileManager] = useState(false);
|
||||
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
||||
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
||||
@@ -777,6 +779,19 @@ export default function ProjectCard({ project }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Claude Code Settings */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-[var(--text-secondary)]">
|
||||
Claude Code Settings{project.claude_code_settings ? " (set)" : ""}<Tooltip text="Configure Claude Code CLI behavior: TUI mode, effort level, focus mode, prompt caching, and more. These override global defaults for this project." />
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* MCP Servers */}
|
||||
{mcpServers.length > 0 && (
|
||||
<div>
|
||||
@@ -1079,6 +1094,17 @@ export default function ProjectCard({ project }: Props) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showClaudeCodeSettingsModal && (
|
||||
<ClaudeCodeSettingsModal
|
||||
settings={project.claude_code_settings}
|
||||
disabled={!isStopped}
|
||||
onSave={async (ccSettings) => {
|
||||
await update({ ...project, claude_code_settings: ccSettings });
|
||||
}}
|
||||
onClose={() => setShowClaudeCodeSettingsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFileManager && (
|
||||
<FileManagerModal
|
||||
projectId={project.id}
|
||||
|
||||
@@ -4,6 +4,7 @@ import AwsSettings from "./AwsSettings";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { useUpdates } from "../../hooks/useUpdates";
|
||||
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
||||
import ClaudeCodeSettingsModal from "../projects/ClaudeCodeSettingsModal";
|
||||
import EnvVarsModal from "../projects/EnvVarsModal";
|
||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||
import type { EnvVar } from "../../lib/types";
|
||||
@@ -18,15 +19,22 @@ export default function SettingsPanel() {
|
||||
const [globalEnvVars, setGlobalEnvVars] = useState<EnvVar[]>(appSettings?.global_custom_env_vars ?? []);
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
||||
const [timezone, setTimezone] = useState(appSettings?.timezone ?? "");
|
||||
const [sshKeyPath, setSshKeyPath] = useState(appSettings?.default_ssh_key_path ?? "");
|
||||
const [gitName, setGitName] = useState(appSettings?.default_git_user_name ?? "");
|
||||
const [gitEmail, setGitEmail] = useState(appSettings?.default_git_user_email ?? "");
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState(false);
|
||||
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
||||
const [showClaudeCodeSettingsModal, setShowClaudeCodeSettingsModal] = useState(false);
|
||||
|
||||
// Sync local state when appSettings change
|
||||
useEffect(() => {
|
||||
setGlobalInstructions(appSettings?.global_claude_instructions ?? "");
|
||||
setGlobalEnvVars(appSettings?.global_custom_env_vars ?? []);
|
||||
setTimezone(appSettings?.timezone ?? "");
|
||||
}, [appSettings?.global_claude_instructions, appSettings?.global_custom_env_vars, appSettings?.timezone]);
|
||||
setSshKeyPath(appSettings?.default_ssh_key_path ?? "");
|
||||
setGitName(appSettings?.default_git_user_name ?? "");
|
||||
setGitEmail(appSettings?.default_git_user_email ?? "");
|
||||
}, [appSettings?.global_claude_instructions, appSettings?.global_custom_env_vars, appSettings?.timezone, appSettings?.default_ssh_key_path, appSettings?.default_git_user_name, appSettings?.default_git_user_email]);
|
||||
|
||||
// Auto-detect timezone on first load if not yet set
|
||||
useEffect(() => {
|
||||
@@ -60,6 +68,60 @@ export default function SettingsPanel() {
|
||||
<DockerSettings />
|
||||
<AwsSettings />
|
||||
|
||||
{/* Default SSH Key Directory */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Default SSH Key Directory<Tooltip text="Global default SSH key directory. Mounted into containers that don't have a per-project SSH path set." /></label>
|
||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||
Mounted into all containers unless overridden by a per-project setting.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={sshKeyPath}
|
||||
onChange={(e) => setSshKeyPath(e.target.value)}
|
||||
onBlur={async () => {
|
||||
if (appSettings) {
|
||||
await saveSettings({ ...appSettings, default_ssh_key_path: sshKeyPath || null });
|
||||
}
|
||||
}}
|
||||
placeholder="~/.ssh"
|
||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default Git Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Default Git Name<Tooltip text="Sets git user.name inside containers. Per-project Git Name takes precedence." /></label>
|
||||
<input
|
||||
type="text"
|
||||
value={gitName}
|
||||
onChange={(e) => setGitName(e.target.value)}
|
||||
onBlur={async () => {
|
||||
if (appSettings) {
|
||||
await saveSettings({ ...appSettings, default_git_user_name: gitName || null });
|
||||
}
|
||||
}}
|
||||
placeholder="Your Name"
|
||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default Git Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Default Git Email<Tooltip text="Sets git user.email inside containers. Per-project Git Email takes precedence." /></label>
|
||||
<input
|
||||
type="text"
|
||||
value={gitEmail}
|
||||
onChange={(e) => setGitEmail(e.target.value)}
|
||||
onBlur={async () => {
|
||||
if (appSettings) {
|
||||
await saveSettings({ ...appSettings, default_git_user_email: gitEmail || null });
|
||||
}
|
||||
}}
|
||||
placeholder="you@example.com"
|
||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Container Timezone */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
||||
@@ -118,6 +180,25 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Claude Code Settings */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Claude Code Settings<Tooltip text="Global defaults for Claude Code CLI behavior (TUI mode, effort, focus mode, caching, etc.). Per-project settings override these." /></label>
|
||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||
Default Claude Code CLI settings applied to all projects. Per-project settings take precedence.
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-[var(--text-secondary)]">
|
||||
{appSettings?.global_claude_code_settings ? "Configured" : "Using defaults"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Web Terminal */}
|
||||
<WebTerminalSettings />
|
||||
|
||||
@@ -189,6 +270,19 @@ export default function SettingsPanel() {
|
||||
onClose={() => setShowEnvVarsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showClaudeCodeSettingsModal && (
|
||||
<ClaudeCodeSettingsModal
|
||||
settings={appSettings?.global_claude_code_settings ?? null}
|
||||
disabled={false}
|
||||
onSave={async (ccSettings) => {
|
||||
if (appSettings) {
|
||||
await saveSettings({ ...appSettings, global_claude_code_settings: ccSettings });
|
||||
}
|
||||
}}
|
||||
onClose={() => setShowClaudeCodeSettingsModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function TerminalTabs() {
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<span className="truncate max-w-[120px]">
|
||||
{session.projectName}{session.sessionType === "bash" ? " (bash)" : ""}
|
||||
<span className="truncate max-w-[140px]">
|
||||
{session.sessionName ?? session.projectName}{session.sessionType === "bash" ? " (bash)" : ""}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -17,10 +17,10 @@ export function useTerminal() {
|
||||
);
|
||||
|
||||
const open = useCallback(
|
||||
async (projectId: string, projectName: string, sessionType: "claude" | "bash" = "claude") => {
|
||||
async (projectId: string, projectName: string, sessionType: "claude" | "bash" = "claude", sessionName?: string) => {
|
||||
const sessionId = crypto.randomUUID();
|
||||
await commands.openTerminalSession(projectId, sessionId, sessionType);
|
||||
addSession({ id: sessionId, projectId, projectName, sessionType });
|
||||
await commands.openTerminalSession(projectId, sessionId, sessionType, sessionName);
|
||||
addSession({ id: sessionId, projectId, projectName, sessionType, sessionName: sessionName ?? null });
|
||||
return sessionId;
|
||||
},
|
||||
[addSession],
|
||||
|
||||
@@ -45,8 +45,8 @@ export const awsSsoRefresh = (projectId: string) =>
|
||||
invoke<void>("aws_sso_refresh", { projectId });
|
||||
|
||||
// Terminal
|
||||
export const openTerminalSession = (projectId: string, sessionId: string, sessionType?: string) =>
|
||||
invoke<void>("open_terminal_session", { projectId, sessionId, sessionType });
|
||||
export const openTerminalSession = (projectId: string, sessionId: string, sessionType?: string, sessionName?: string) =>
|
||||
invoke<void>("open_terminal_session", { projectId, sessionId, sessionType, sessionName });
|
||||
export const terminalInput = (sessionId: string, data: number[]) =>
|
||||
invoke<void>("terminal_input", { sessionId, data });
|
||||
export const terminalResize = (sessionId: string, cols: number, rows: number) =>
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface Project {
|
||||
port_mappings: PortMapping[];
|
||||
claude_instructions: string | null;
|
||||
enabled_mcp_servers: string[];
|
||||
claude_code_settings: ClaudeCodeSettings | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -73,6 +74,17 @@ export interface OpenAiCompatibleConfig {
|
||||
model_id: string | null;
|
||||
}
|
||||
|
||||
export interface ClaudeCodeSettings {
|
||||
tui_mode: string | null;
|
||||
effort: string | null;
|
||||
auto_scroll_disabled: boolean;
|
||||
focus_mode: boolean;
|
||||
show_thinking_summaries: boolean;
|
||||
enable_session_recap: boolean;
|
||||
env_scrub: boolean;
|
||||
prompt_caching_1h: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerInfo {
|
||||
container_id: string;
|
||||
project_id: string;
|
||||
@@ -93,6 +105,7 @@ export interface TerminalSession {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
sessionType: "claude" | "bash";
|
||||
sessionName: string | null;
|
||||
}
|
||||
|
||||
export type ImageSource = "registry" | "local_build" | "custom";
|
||||
@@ -120,6 +133,7 @@ export interface AppSettings {
|
||||
dismissed_image_digest: string | null;
|
||||
web_terminal: WebTerminalSettings;
|
||||
stt: SttSettings;
|
||||
global_claude_code_settings: ClaudeCodeSettings | null;
|
||||
}
|
||||
|
||||
export interface SttSettings {
|
||||
|
||||
Reference in New Issue
Block a user