Story 37: Editor Command for Worktrees
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
AgentStatusValue,
|
||||
} from "../api/agents";
|
||||
import { agentsApi, subscribeAgentStream } from "../api/agents";
|
||||
import { settingsApi } from "../api/settings";
|
||||
import type { UpcomingStory } from "../api/workflow";
|
||||
|
||||
const { useCallback, useEffect, useRef, useState } = React;
|
||||
@@ -171,6 +172,72 @@ function DiffCommand({
|
||||
);
|
||||
}
|
||||
|
||||
function EditorCommand({
|
||||
worktreePath,
|
||||
editorCommand,
|
||||
}: {
|
||||
worktreePath: string;
|
||||
editorCommand: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const command = `${editorCommand} "${worktreePath}"`;
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback: select text for manual copy
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
<code
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: "0.7em",
|
||||
color: "#8b949e",
|
||||
background: "#0d1117",
|
||||
padding: "4px 8px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #21262d",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{command}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
padding: "3px 8px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #30363d",
|
||||
background: copied ? "#238636" : "#21262d",
|
||||
color: copied ? "#fff" : "#8b949e",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.7em",
|
||||
fontWeight: 600,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{copied ? "Copied" : "Open"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentPanel({ stories }: AgentPanelProps) {
|
||||
const [agents, setAgents] = useState<Record<string, AgentState>>({});
|
||||
const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
|
||||
@@ -178,10 +245,13 @@ export function AgentPanel({ stories }: AgentPanelProps) {
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
|
||||
const [selectorStory, setSelectorStory] = useState<string | null>(null);
|
||||
const [editorCommand, setEditorCommand] = useState<string | null>(null);
|
||||
const [editorInput, setEditorInput] = useState<string>("");
|
||||
const [editingEditor, setEditingEditor] = useState(false);
|
||||
const cleanupRefs = useRef<Record<string, () => void>>({});
|
||||
const logEndRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
// Load roster and existing agents on mount
|
||||
// Load roster, existing agents, and editor preference on mount
|
||||
useEffect(() => {
|
||||
agentsApi
|
||||
.getAgentConfig()
|
||||
@@ -211,6 +281,14 @@ export function AgentPanel({ stories }: AgentPanelProps) {
|
||||
})
|
||||
.catch((err) => console.error("Failed to load agents:", err));
|
||||
|
||||
settingsApi
|
||||
.getEditorCommand()
|
||||
.then((s) => {
|
||||
setEditorCommand(s.editor_command);
|
||||
setEditorInput(s.editor_command ?? "");
|
||||
})
|
||||
.catch((err) => console.error("Failed to load editor command:", err));
|
||||
|
||||
return () => {
|
||||
for (const cleanup of Object.values(cleanupRefs.current)) {
|
||||
cleanup();
|
||||
@@ -347,6 +425,19 @@ export function AgentPanel({ stories }: AgentPanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEditor = async () => {
|
||||
try {
|
||||
const trimmed = editorInput.trim() || null;
|
||||
const result = await settingsApi.setEditorCommand(trimmed);
|
||||
setEditorCommand(result.editor_command);
|
||||
setEditorInput(result.editor_command ?? "");
|
||||
setEditingEditor(false);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setActionError(`Failed to save editor: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Get all active agent keys for a story. */
|
||||
const getActiveKeysForStory = (storyId: string): string[] => {
|
||||
return Object.keys(agents).filter((key) => {
|
||||
@@ -404,6 +495,81 @@ export function AgentPanel({ stories }: AgentPanelProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Editor preference */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span style={{ fontSize: "0.75em", color: "#666" }}>Editor:</span>
|
||||
{editingEditor ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={editorInput}
|
||||
onChange={(e) => setEditorInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSaveEditor();
|
||||
if (e.key === "Escape") setEditingEditor(false);
|
||||
}}
|
||||
placeholder="zed, code, cursor..."
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
background: "#111",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
color: "#ccc",
|
||||
padding: "2px 6px",
|
||||
width: "120px",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveEditor}
|
||||
style={{
|
||||
fontSize: "0.7em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #238636",
|
||||
background: "#238636",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingEditor(false)}
|
||||
style={{
|
||||
fontSize: "0.7em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #444",
|
||||
background: "none",
|
||||
color: "#888",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingEditor(true)}
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
background: "none",
|
||||
border: "1px solid #333",
|
||||
borderRadius: "4px",
|
||||
color: editorCommand ? "#aaa" : "#555",
|
||||
cursor: "pointer",
|
||||
padding: "2px 8px",
|
||||
fontFamily: editorCommand ? "monospace" : "inherit",
|
||||
}}
|
||||
>
|
||||
{editorCommand ?? "Set editor..."}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Roster badges */}
|
||||
{roster.length > 0 && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user