Compare commits
3 Commits
v0.1.84-ma
...
v0.1.87-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 090aad6bc6 | |||
| c023d80c86 | |||
| 33f02e65c0 |
@@ -35,7 +35,7 @@ pub struct Project {
|
|||||||
pub bedrock_config: Option<BedrockConfig>,
|
pub bedrock_config: Option<BedrockConfig>,
|
||||||
pub allow_docker_access: bool,
|
pub allow_docker_access: 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 +100,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,
|
||||||
|
|||||||
@@ -30,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";
|
||||||
|
|
||||||
@@ -54,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 ?? "");
|
||||||
@@ -309,7 +313,40 @@ 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"
|
||||||
|
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) => (
|
||||||
@@ -385,16 +422,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 */}
|
||||||
@@ -869,3 +924,4 @@ function ActionButton({
|
|||||||
</button>
|
</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>
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { useTerminal } from "../../hooks/useTerminal";
|
import { useTerminal } from "../../hooks/useTerminal";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
|
||||||
import { useVoice } from "../../hooks/useVoice";
|
|
||||||
import { UrlDetector } from "../../lib/urlDetector";
|
import { UrlDetector } from "../../lib/urlDetector";
|
||||||
import UrlToast from "./UrlToast";
|
import UrlToast from "./UrlToast";
|
||||||
|
|
||||||
@@ -24,9 +22,6 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const webglRef = useRef<WebglAddon | null>(null);
|
const webglRef = useRef<WebglAddon | null>(null);
|
||||||
const detectorRef = useRef<UrlDetector | null>(null);
|
const detectorRef = useRef<UrlDetector | null>(null);
|
||||||
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
|
||||||
const { appSettings } = useSettings();
|
|
||||||
|
|
||||||
const voice = useVoice(sessionId, appSettings?.default_microphone);
|
|
||||||
|
|
||||||
const [detectedUrl, setDetectedUrl] = useState<string | null>(null);
|
const [detectedUrl, setDetectedUrl] = useState<string | null>(null);
|
||||||
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
||||||
@@ -205,7 +200,6 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
try { webglRef.current?.dispose(); } catch { /* may already be disposed */ }
|
try { webglRef.current?.dispose(); } catch { /* may already be disposed */ }
|
||||||
webglRef.current = null;
|
webglRef.current = null;
|
||||||
term.dispose();
|
term.dispose();
|
||||||
voice.stop();
|
|
||||||
};
|
};
|
||||||
}, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
@@ -290,32 +284,6 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
{imagePasteMsg}
|
{imagePasteMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
onClick={voice.toggle}
|
|
||||||
title={
|
|
||||||
voice.state === "active"
|
|
||||||
? "Voice active — click to stop"
|
|
||||||
: voice.error
|
|
||||||
? `Voice error: ${voice.error}`
|
|
||||||
: "Enable voice input for /voice mode"
|
|
||||||
}
|
|
||||||
className={`absolute bottom-4 left-4 z-50 px-3 py-1.5 rounded-md text-xs font-medium border shadow-lg transition-colors cursor-pointer ${
|
|
||||||
voice.state === "active"
|
|
||||||
? "bg-[#1a3a2a] text-[#3fb950] border-[#238636] hover:bg-[#243b2a]"
|
|
||||||
: voice.state === "starting"
|
|
||||||
? "bg-[#1f2937] text-[#d29922] border-[#30363d] opacity-75"
|
|
||||||
: voice.state === "error"
|
|
||||||
? "bg-[#3a1a1a] text-[#ff7b72] border-[#da3633] hover:bg-[#4a2020]"
|
|
||||||
: "bg-[#1f2937] text-[#b1bac4] border-[#30363d] hover:bg-[#2d3748] hover:text-[#e6edf3]"
|
|
||||||
}`}
|
|
||||||
disabled={voice.state === "starting"}
|
|
||||||
>
|
|
||||||
{voice.state === "active"
|
|
||||||
? "Mic On"
|
|
||||||
: voice.state === "starting"
|
|
||||||
? "Mic..."
|
|
||||||
: "Mic Off"}
|
|
||||||
</button>
|
|
||||||
{!isAtBottom && (
|
{!isAtBottom && (
|
||||||
<button
|
<button
|
||||||
onClick={handleScrollToBottom}
|
onClick={handleScrollToBottom}
|
||||||
|
|||||||
Reference in New Issue
Block a user