Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 429acd2fb5 | |||
| c853f2676d | |||
| 090aad6bc6 | |||
| c023d80c86 |
@@ -40,6 +40,54 @@ After tasks run, check notifications with `triple-c-scheduler notifications` and
|
|||||||
### Timezone
|
### Timezone
|
||||||
Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#;
|
Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#;
|
||||||
|
|
||||||
|
const MISSION_CONTROL_GLOBAL_INSTRUCTIONS: &str = r#"## Mission Control
|
||||||
|
|
||||||
|
The `/workspace/mission-control/` directory contains **Flight Control** — an AI-first development methodology for structured project management. Use it for all project work.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
- **Mission Control is a tool, not a project.** It provides skills and methodology for managing other projects.
|
||||||
|
- All Flight Control skills live in `/workspace/mission-control/.claude/skills/`
|
||||||
|
- The projects registry at `/workspace/mission-control/projects.md` lists all active projects
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
When working on any project that has a `.flightops/` directory, follow the Flight Control methodology:
|
||||||
|
1. Read the project's `.flightops/ARTIFACTS.md` to understand artifact storage
|
||||||
|
2. Read `.flightops/FLIGHT_OPERATIONS.md` for the implementation workflow
|
||||||
|
3. Use Mission Control skills for planning and execution
|
||||||
|
|
||||||
|
### Available Skills
|
||||||
|
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `/init-project` | Setting up a new project for Flight Control |
|
||||||
|
| `/mission` | Defining new work outcomes (days-to-weeks scope) |
|
||||||
|
| `/flight` | Creating technical specs from missions (hours-to-days scope) |
|
||||||
|
| `/leg` | Generating implementation steps from flights (minutes-to-hours scope) |
|
||||||
|
| `/agentic-workflow` | Executing legs with multi-agent workflow (implement, review, commit) |
|
||||||
|
| `/flight-debrief` | Post-flight analysis after a flight lands |
|
||||||
|
| `/mission-debrief` | Post-mission retrospective after completion |
|
||||||
|
| `/daily-briefing` | Cross-project status report |
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- **Planning skills produce artifacts only** — never modify source code directly
|
||||||
|
- **Phase gates require human confirmation** — missions before flights, flights before legs
|
||||||
|
- **Legs are immutable once in-flight** — create new ones instead of modifying
|
||||||
|
- **`/agentic-workflow` orchestrates implementation** — it spawns separate Developer and Reviewer agents
|
||||||
|
- **Artifacts live in the target project** — not in mission-control"#;
|
||||||
|
|
||||||
|
const MISSION_CONTROL_PROJECT_INSTRUCTIONS: &str = r#"## Flight Operations
|
||||||
|
|
||||||
|
This project uses [Flight Control](https://github.com/msieurthenardier/mission-control) for structured development.
|
||||||
|
|
||||||
|
**Before any mission/flight/leg work, read these files in order:**
|
||||||
|
1. `.flightops/README.md` — What the flightops directory contains
|
||||||
|
2. `.flightops/FLIGHT_OPERATIONS.md` — **The workflow you MUST follow**
|
||||||
|
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
|
||||||
|
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
|
||||||
|
|
||||||
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
||||||
/// instructions, appending port mapping docs, and appending scheduler docs.
|
/// instructions, appending port mapping docs, and appending scheduler docs.
|
||||||
/// Used by both create_container() and container_needs_recreation() to ensure
|
/// Used by both create_container() and container_needs_recreation() to ensure
|
||||||
@@ -48,8 +96,13 @@ fn build_claude_instructions(
|
|||||||
global_instructions: Option<&str>,
|
global_instructions: Option<&str>,
|
||||||
project_instructions: Option<&str>,
|
project_instructions: Option<&str>,
|
||||||
port_mappings: &[PortMapping],
|
port_mappings: &[PortMapping],
|
||||||
|
mission_control_enabled: bool,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut combined = merge_claude_instructions(global_instructions, project_instructions);
|
let mut combined = merge_claude_instructions(
|
||||||
|
global_instructions,
|
||||||
|
project_instructions,
|
||||||
|
mission_control_enabled,
|
||||||
|
);
|
||||||
|
|
||||||
if !port_mappings.is_empty() {
|
if !port_mappings.is_empty() {
|
||||||
let mut port_lines: Vec<String> = Vec::new();
|
let mut port_lines: Vec<String> = Vec::new();
|
||||||
@@ -116,14 +169,37 @@ fn merge_custom_env_vars(global: &[EnvVar], project: &[EnvVar]) -> Vec<EnvVar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Merge global and per-project Claude instructions into a single string.
|
/// Merge global and per-project Claude instructions into a single string.
|
||||||
|
/// When mission_control_enabled is true, appends Mission Control global
|
||||||
|
/// instructions after global and project instructions after project.
|
||||||
fn merge_claude_instructions(
|
fn merge_claude_instructions(
|
||||||
global_instructions: Option<&str>,
|
global_instructions: Option<&str>,
|
||||||
project_instructions: Option<&str>,
|
project_instructions: Option<&str>,
|
||||||
|
mission_control_enabled: bool,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
match (global_instructions, project_instructions) {
|
// Build the global portion (user global + optional MC global)
|
||||||
|
let global_part = if mission_control_enabled {
|
||||||
|
match global_instructions {
|
||||||
|
Some(g) => Some(format!("{}\n\n{}", g, MISSION_CONTROL_GLOBAL_INSTRUCTIONS)),
|
||||||
|
None => Some(MISSION_CONTROL_GLOBAL_INSTRUCTIONS.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
global_instructions.map(|g| g.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the project portion (user project + optional MC project)
|
||||||
|
let project_part = if mission_control_enabled {
|
||||||
|
match project_instructions {
|
||||||
|
Some(p) => Some(format!("{}\n\n{}", p, MISSION_CONTROL_PROJECT_INSTRUCTIONS)),
|
||||||
|
None => Some(MISSION_CONTROL_PROJECT_INSTRUCTIONS.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
project_instructions.map(|p| p.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
match (global_part, project_part) {
|
||||||
(Some(g), Some(p)) => Some(format!("{}\n\n{}", g, p)),
|
(Some(g), Some(p)) => Some(format!("{}\n\n{}", g, p)),
|
||||||
(Some(g), None) => Some(g.to_string()),
|
(Some(g), None) => Some(g),
|
||||||
(None, Some(p)) => Some(p.to_string()),
|
(None, Some(p)) => Some(p),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,11 +502,17 @@ pub async fn create_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mission Control env var
|
||||||
|
if project.mission_control_enabled {
|
||||||
|
env_vars.push("MISSION_CONTROL_ENABLED=1".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// Claude instructions (global + per-project, plus port mapping info + scheduler docs)
|
// Claude instructions (global + per-project, plus port mapping info + scheduler docs)
|
||||||
let combined_instructions = build_claude_instructions(
|
let combined_instructions = build_claude_instructions(
|
||||||
global_claude_instructions,
|
global_claude_instructions,
|
||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
|
project.mission_control_enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref instructions) = combined_instructions {
|
if let Some(ref instructions) = combined_instructions {
|
||||||
@@ -567,6 +649,7 @@ pub async fn create_container(
|
|||||||
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
||||||
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
|
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
|
||||||
labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers));
|
labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers));
|
||||||
|
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
|
||||||
|
|
||||||
let host_config = HostConfig {
|
let host_config = HostConfig {
|
||||||
mounts: Some(mounts),
|
mounts: Some(mounts),
|
||||||
@@ -885,11 +968,20 @@ pub async fn container_needs_recreation(
|
|||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Mission Control ────────────────────────────────────────────────────
|
||||||
|
let expected_mc = project.mission_control_enabled.to_string();
|
||||||
|
let container_mc = get_label("triple-c.mission-control").unwrap_or_else(|| "false".to_string());
|
||||||
|
if container_mc != expected_mc {
|
||||||
|
log::info!("Mission Control mismatch (container={:?}, expected={:?})", container_mc, expected_mc);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Claude instructions ───────────────────────────────────────────────
|
// ── Claude instructions ───────────────────────────────────────────────
|
||||||
let expected_instructions = build_claude_instructions(
|
let expected_instructions = build_claude_instructions(
|
||||||
global_claude_instructions,
|
global_claude_instructions,
|
||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
|
project.mission_control_enabled,
|
||||||
);
|
);
|
||||||
let container_instructions = get_env("CLAUDE_INSTRUCTIONS");
|
let container_instructions = get_env("CLAUDE_INSTRUCTIONS");
|
||||||
if container_instructions.as_deref() != expected_instructions.as_deref() {
|
if container_instructions.as_deref() != expected_instructions.as_deref() {
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ pub struct Project {
|
|||||||
pub auth_mode: AuthMode,
|
pub auth_mode: AuthMode,
|
||||||
pub bedrock_config: Option<BedrockConfig>,
|
pub bedrock_config: Option<BedrockConfig>,
|
||||||
pub allow_docker_access: bool,
|
pub allow_docker_access: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mission_control_enabled: bool,
|
||||||
pub ssh_key_path: Option<String>,
|
pub ssh_key_path: Option<String>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, default)]
|
||||||
pub git_token: Option<String>,
|
pub git_token: Option<String>,
|
||||||
pub git_user_name: Option<String>,
|
pub git_user_name: Option<String>,
|
||||||
pub git_user_email: Option<String>,
|
pub git_user_email: Option<String>,
|
||||||
@@ -100,14 +102,14 @@ impl Default for BedrockAuthMethod {
|
|||||||
pub struct BedrockConfig {
|
pub struct BedrockConfig {
|
||||||
pub auth_method: BedrockAuthMethod,
|
pub auth_method: BedrockAuthMethod,
|
||||||
pub aws_region: String,
|
pub aws_region: String,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, default)]
|
||||||
pub aws_access_key_id: Option<String>,
|
pub aws_access_key_id: Option<String>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, default)]
|
||||||
pub aws_secret_access_key: Option<String>,
|
pub aws_secret_access_key: Option<String>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, default)]
|
||||||
pub aws_session_token: Option<String>,
|
pub aws_session_token: Option<String>,
|
||||||
pub aws_profile: Option<String>,
|
pub aws_profile: Option<String>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, default)]
|
||||||
pub aws_bearer_token: Option<String>,
|
pub aws_bearer_token: Option<String>,
|
||||||
pub model_id: Option<String>,
|
pub model_id: Option<String>,
|
||||||
pub disable_prompt_caching: bool,
|
pub disable_prompt_caching: bool,
|
||||||
@@ -125,6 +127,7 @@ impl Project {
|
|||||||
auth_mode: AuthMode::default(),
|
auth_mode: AuthMode::default(),
|
||||||
bedrock_config: None,
|
bedrock_config: None,
|
||||||
allow_docker_access: false,
|
allow_docker_access: false,
|
||||||
|
mission_control_enabled: false,
|
||||||
ssh_key_path: None,
|
ssh_key_path: None,
|
||||||
git_token: None,
|
git_token: None,
|
||||||
git_user_name: None,
|
git_user_name: None,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import type { Project, ProjectPath, AuthMode, BedrockConfig, BedrockAuthMethod }
|
|||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useMcpServers } from "../../hooks/useMcpServers";
|
import { useMcpServers } from "../../hooks/useMcpServers";
|
||||||
import { useTerminal } from "../../hooks/useTerminal";
|
import { useTerminal } from "../../hooks/useTerminal";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
|
||||||
import { useVoice } from "../../hooks/useVoice";
|
|
||||||
import { useAppState } from "../../store/appState";
|
import { useAppState } from "../../store/appState";
|
||||||
import EnvVarsModal from "./EnvVarsModal";
|
import EnvVarsModal from "./EnvVarsModal";
|
||||||
import PortMappingsModal from "./PortMappingsModal";
|
import PortMappingsModal from "./PortMappingsModal";
|
||||||
@@ -23,15 +21,6 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const { start, stop, rebuild, remove, update } = useProjects();
|
const { start, stop, rebuild, remove, update } = useProjects();
|
||||||
const { mcpServers } = useMcpServers();
|
const { mcpServers } = useMcpServers();
|
||||||
const { open: openTerminal } = useTerminal();
|
const { open: openTerminal } = useTerminal();
|
||||||
const { appSettings } = useSettings();
|
|
||||||
const sessions = useAppState(s => s.sessions);
|
|
||||||
const activeSessionId = useAppState(s => s.activeSessionId);
|
|
||||||
|
|
||||||
// Find the active terminal session for this project (prefer the currently viewed one)
|
|
||||||
const projectSession = sessions.find(s => s.projectId === project.id && s.id === activeSessionId)
|
|
||||||
?? sessions.find(s => s.projectId === project.id);
|
|
||||||
const voice = useVoice(projectSession?.id ?? "", appSettings?.default_microphone);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
@@ -41,6 +30,9 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
||||||
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
||||||
const [operationCompleted, setOperationCompleted] = useState(false);
|
const [operationCompleted, setOperationCompleted] = useState(false);
|
||||||
|
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false);
|
||||||
|
const [isEditingName, setIsEditingName] = useState(false);
|
||||||
|
const [editName, setEditName] = useState(project.name);
|
||||||
const isSelected = selectedProjectId === project.id;
|
const isSelected = selectedProjectId === project.id;
|
||||||
const isStopped = project.status === "stopped" || project.status === "error";
|
const isStopped = project.status === "stopped" || project.status === "error";
|
||||||
|
|
||||||
@@ -65,6 +57,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
|
|
||||||
// Sync local state when project prop changes (e.g., after save or external update)
|
// Sync local state when project prop changes (e.g., after save or external update)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setEditName(project.name);
|
||||||
setPaths(project.paths ?? []);
|
setPaths(project.paths ?? []);
|
||||||
setSshKeyPath(project.ssh_key_path ?? "");
|
setSshKeyPath(project.ssh_key_path ?? "");
|
||||||
setGitName(project.git_user_name ?? "");
|
setGitName(project.git_user_name ?? "");
|
||||||
@@ -320,7 +313,41 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
|
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
|
||||||
<span className="text-sm font-medium truncate flex-1">{project.name}</span>
|
{isEditingName ? (
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
setIsEditingName(false);
|
||||||
|
const trimmed = editName.trim();
|
||||||
|
if (trimmed && trimmed !== project.name) {
|
||||||
|
try {
|
||||||
|
await update({ ...project, name: trimmed });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to rename project:", err);
|
||||||
|
setEditName(project.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEditName(project.name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") (e.target as HTMLInputElement).blur();
|
||||||
|
if (e.key === "Escape") { setEditName(project.name); setIsEditingName(false); }
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="text-sm font-medium flex-1 min-w-0 px-1 py-0 bg-[var(--bg-primary)] border border-[var(--accent)] rounded text-[var(--text-primary)] focus:outline-none"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium truncate flex-1 cursor-text"
|
||||||
|
title="Double-click to rename"
|
||||||
|
onDoubleClick={(e) => { e.stopPropagation(); setIsEditingName(true); }}
|
||||||
|
>
|
||||||
|
{project.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 ml-4 space-y-0.5">
|
<div className="mt-0.5 ml-4 space-y-0.5">
|
||||||
{project.paths.map((pp, i) => (
|
{project.paths.map((pp, i) => (
|
||||||
@@ -382,9 +409,6 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
<>
|
<>
|
||||||
<ActionButton onClick={handleStop} disabled={loading} label="Stop" />
|
<ActionButton onClick={handleStop} disabled={loading} label="Stop" />
|
||||||
<ActionButton onClick={handleOpenTerminal} disabled={loading} label="Terminal" accent />
|
<ActionButton onClick={handleOpenTerminal} disabled={loading} label="Terminal" accent />
|
||||||
{projectSession && (
|
|
||||||
<MicButton voice={voice} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -399,16 +423,34 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
disabled={false}
|
disabled={false}
|
||||||
label={showConfig ? "Hide" : "Config"}
|
label={showConfig ? "Hide" : "Config"}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
{showRemoveConfirm ? (
|
||||||
onClick={async () => {
|
<span className="inline-flex items-center gap-1 text-xs">
|
||||||
if (confirm(`Remove project "${project.name}"?`)) {
|
<span className="text-[var(--text-secondary)]">Remove?</span>
|
||||||
await remove(project.id);
|
<button
|
||||||
}
|
onClick={async (e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
disabled={loading}
|
setShowRemoveConfirm(false);
|
||||||
label="Remove"
|
await remove(project.id);
|
||||||
danger
|
}}
|
||||||
/>
|
className="px-1.5 py-0.5 rounded text-white bg-[var(--error)] hover:opacity-80 transition-colors"
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setShowRemoveConfirm(false); }}
|
||||||
|
className="px-1.5 py-0.5 rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<ActionButton
|
||||||
|
onClick={() => setShowRemoveConfirm(true)}
|
||||||
|
disabled={loading}
|
||||||
|
label="Remove"
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Config panel */}
|
{/* Config panel */}
|
||||||
@@ -590,6 +632,28 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mission Control toggle */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-[var(--text-secondary)]">Mission Control</label>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await update({ ...project, mission_control_enabled: !project.mission_control_enabled });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update Mission Control setting:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!isStopped}
|
||||||
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
||||||
|
project.mission_control_enabled
|
||||||
|
? "bg-[var(--success)] text-white"
|
||||||
|
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.mission_control_enabled ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">
|
<label className="text-xs text-[var(--text-secondary)]">
|
||||||
@@ -884,34 +948,3 @@ function ActionButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MicButton({ voice }: { voice: ReturnType<typeof useVoice> }) {
|
|
||||||
const color =
|
|
||||||
voice.state === "active"
|
|
||||||
? "text-[var(--success)] hover:text-[var(--success)]"
|
|
||||||
: voice.state === "starting"
|
|
||||||
? "text-[var(--warning)] opacity-75"
|
|
||||||
: voice.state === "error"
|
|
||||||
? "text-[var(--error)] hover:text-[var(--error)]"
|
|
||||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={(e) => { e.stopPropagation(); voice.toggle(); }}
|
|
||||||
disabled={voice.state === "starting"}
|
|
||||||
title={
|
|
||||||
voice.state === "active"
|
|
||||||
? "Voice active — click to stop"
|
|
||||||
: voice.error
|
|
||||||
? `Voice error: ${voice.error}`
|
|
||||||
: "Enable voice input for /voice mode"
|
|
||||||
}
|
|
||||||
className={`text-xs px-2 py-0.5 rounded transition-colors disabled:opacity-50 ${color} hover:bg-[var(--bg-primary)]`}
|
|
||||||
>
|
|
||||||
{voice.state === "active"
|
|
||||||
? "Mic On"
|
|
||||||
: voice.state === "starting"
|
|
||||||
? "Mic..."
|
|
||||||
: "Mic Off"}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import ApiKeyInput from "./ApiKeyInput";
|
import ApiKeyInput from "./ApiKeyInput";
|
||||||
import DockerSettings from "./DockerSettings";
|
import DockerSettings from "./DockerSettings";
|
||||||
import AwsSettings from "./AwsSettings";
|
import AwsSettings from "./AwsSettings";
|
||||||
import MicrophoneSettings from "./MicrophoneSettings";
|
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
import { useUpdates } from "../../hooks/useUpdates";
|
import { useUpdates } from "../../hooks/useUpdates";
|
||||||
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
||||||
@@ -60,8 +59,6 @@ export default function SettingsPanel() {
|
|||||||
<DockerSettings />
|
<DockerSettings />
|
||||||
<AwsSettings />
|
<AwsSettings />
|
||||||
|
|
||||||
<MicrophoneSettings />
|
|
||||||
|
|
||||||
{/* Container Timezone */}
|
{/* Container Timezone */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Container Timezone</label>
|
<label className="block text-sm font-medium mb-1">Container Timezone</label>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface Project {
|
|||||||
auth_mode: AuthMode;
|
auth_mode: AuthMode;
|
||||||
bedrock_config: BedrockConfig | null;
|
bedrock_config: BedrockConfig | null;
|
||||||
allow_docker_access: boolean;
|
allow_docker_access: boolean;
|
||||||
|
mission_control_enabled: boolean;
|
||||||
ssh_key_path: string | null;
|
ssh_key_path: string | null;
|
||||||
git_token: string | null;
|
git_token: string | null;
|
||||||
git_user_name: string | null;
|
git_user_name: string | null;
|
||||||
|
|||||||
@@ -116,6 +116,24 @@ if [ -n "$CLAUDE_INSTRUCTIONS" ]; then
|
|||||||
unset CLAUDE_INSTRUCTIONS
|
unset CLAUDE_INSTRUCTIONS
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Mission Control setup ───────────────────────────────────────────────────
|
||||||
|
if [ "$MISSION_CONTROL_ENABLED" = "1" ]; then
|
||||||
|
MC_HOME="/home/claude/mission-control"
|
||||||
|
MC_LINK="/workspace/mission-control"
|
||||||
|
if [ ! -d "$MC_HOME/.git" ]; then
|
||||||
|
echo "entrypoint: cloning mission-control..."
|
||||||
|
su -s /bin/bash claude -c \
|
||||||
|
'git clone https://github.com/msieurthenardier/mission-control.git /home/claude/mission-control' \
|
||||||
|
|| echo "entrypoint: warning — failed to clone mission-control"
|
||||||
|
else
|
||||||
|
echo "entrypoint: mission-control already present, skipping clone"
|
||||||
|
fi
|
||||||
|
# Symlink into workspace so Claude sees it at /workspace/mission-control
|
||||||
|
ln -sfn "$MC_HOME" "$MC_LINK"
|
||||||
|
chown -h claude:claude "$MC_LINK"
|
||||||
|
unset MISSION_CONTROL_ENABLED
|
||||||
|
fi
|
||||||
|
|
||||||
# ── MCP server configuration ────────────────────────────────────────────────
|
# ── MCP server configuration ────────────────────────────────────────────────
|
||||||
# Merge MCP server config into ~/.claude.json (preserves existing keys like
|
# Merge MCP server config into ~/.claude.json (preserves existing keys like
|
||||||
# OAuth tokens). Creates the file if it doesn't exist.
|
# OAuth tokens). Creates the file if it doesn't exist.
|
||||||
|
|||||||
Reference in New Issue
Block a user