mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:46:58 +10:00
178 lines
26 KiB
TypeScript
178 lines
26 KiB
TypeScript
|
|
import chalk from 'chalk';
|
||
|
|
import figures from 'figures';
|
||
|
|
import * as React from 'react';
|
||
|
|
import { useCallback, useMemo, useState } from 'react';
|
||
|
|
import { useSetAppState } from 'src/state/AppState.js';
|
||
|
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||
|
|
import { Box, Text } from '../../ink.js';
|
||
|
|
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||
|
|
import type { Tools } from '../../Tool.js';
|
||
|
|
import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js';
|
||
|
|
import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js';
|
||
|
|
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||
|
|
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js';
|
||
|
|
import { ColorPicker } from './ColorPicker.js';
|
||
|
|
import { ModelSelector } from './ModelSelector.js';
|
||
|
|
import { ToolSelector } from './ToolSelector.js';
|
||
|
|
import { getAgentSourceDisplayName } from './utils.js';
|
||
|
|
type Props = {
|
||
|
|
agent: AgentDefinition;
|
||
|
|
tools: Tools;
|
||
|
|
onSaved: (message: string) => void;
|
||
|
|
onBack: () => void;
|
||
|
|
};
|
||
|
|
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
|
||
|
|
type SaveChanges = {
|
||
|
|
tools?: string[];
|
||
|
|
color?: AgentColorName;
|
||
|
|
model?: string;
|
||
|
|
};
|
||
|
|
export function AgentEditor({
|
||
|
|
agent,
|
||
|
|
tools,
|
||
|
|
onSaved,
|
||
|
|
onBack
|
||
|
|
}: Props): React.ReactNode {
|
||
|
|
const setAppState = useSetAppState();
|
||
|
|
const [editMode, setEditMode] = useState<EditMode>('menu');
|
||
|
|
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [selectedColor, setSelectedColor] = useState<AgentColorName | undefined>(agent.color as AgentColorName | undefined);
|
||
|
|
const handleOpenInEditor = useCallback(async () => {
|
||
|
|
const filePath = getActualAgentFilePath(agent);
|
||
|
|
const result = await editFileInEditor(filePath);
|
||
|
|
if (result.error) {
|
||
|
|
setError(result.error);
|
||
|
|
} else {
|
||
|
|
onSaved(`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`);
|
||
|
|
}
|
||
|
|
}, [agent, onSaved]);
|
||
|
|
const handleSave = useCallback(async (changes: SaveChanges = {}) => {
|
||
|
|
const {
|
||
|
|
tools: newTools,
|
||
|
|
color: newColor,
|
||
|
|
model: newModel
|
||
|
|
} = changes;
|
||
|
|
const finalColor = newColor ?? selectedColor;
|
||
|
|
const hasToolsChanged = newTools !== undefined;
|
||
|
|
const hasModelChanged = newModel !== undefined;
|
||
|
|
const hasColorChanged = finalColor !== agent.color;
|
||
|
|
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
// Only custom/plugin agents can be edited
|
||
|
|
// this is for type safety; the UI shouldn't allow editing otherwise
|
||
|
|
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
|
||
|
|
if (hasColorChanged && finalColor) {
|
||
|
|
setAgentColor(agent.agentType, finalColor);
|
||
|
|
}
|
||
|
|
setAppState(state => {
|
||
|
|
const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? {
|
||
|
|
...a,
|
||
|
|
tools: newTools ?? a.tools,
|
||
|
|
color: finalColor,
|
||
|
|
model: newModel ?? a.model
|
||
|
|
} : a);
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
agentDefinitions: {
|
||
|
|
...state.agentDefinitions,
|
||
|
|
activeAgents: getActiveAgentsFromList(allAgents),
|
||
|
|
allAgents
|
||
|
|
}
|
||
|
|
};
|
||
|
|
});
|
||
|
|
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`);
|
||
|
|
return true;
|
||
|
|
} catch (err) {
|
||
|
|
setError(err instanceof Error ? err.message : 'Failed to save agent');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [agent, selectedColor, onSaved, setAppState]);
|
||
|
|
const menuItems = useMemo(() => [{
|
||
|
|
label: 'Open in editor',
|
||
|
|
action: handleOpenInEditor
|
||
|
|
}, {
|
||
|
|
label: 'Edit tools',
|
||
|
|
action: () => setEditMode('edit-tools')
|
||
|
|
}, {
|
||
|
|
label: 'Edit model',
|
||
|
|
action: () => setEditMode('edit-model')
|
||
|
|
}, {
|
||
|
|
label: 'Edit color',
|
||
|
|
action: () => setEditMode('edit-color')
|
||
|
|
}], [handleOpenInEditor]);
|
||
|
|
const handleEscape = useCallback(() => {
|
||
|
|
setError(null);
|
||
|
|
if (editMode === 'menu') {
|
||
|
|
onBack();
|
||
|
|
} else {
|
||
|
|
setEditMode('menu');
|
||
|
|
}
|
||
|
|
}, [editMode, onBack]);
|
||
|
|
const handleMenuKeyDown = useCallback((e: KeyboardEvent) => {
|
||
|
|
if (e.key === 'up') {
|
||
|
|
e.preventDefault();
|
||
|
|
setSelectedMenuIndex(index => Math.max(0, index - 1));
|
||
|
|
} else if (e.key === 'down') {
|
||
|
|
e.preventDefault();
|
||
|
|
setSelectedMenuIndex(index_0 => Math.min(menuItems.length - 1, index_0 + 1));
|
||
|
|
} else if (e.key === 'return') {
|
||
|
|
e.preventDefault();
|
||
|
|
const selectedItem = menuItems[selectedMenuIndex];
|
||
|
|
if (selectedItem) {
|
||
|
|
void selectedItem.action();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [menuItems, selectedMenuIndex]);
|
||
|
|
useKeybinding('confirm:no', handleEscape, {
|
||
|
|
context: 'Confirmation'
|
||
|
|
});
|
||
|
|
const renderMenu = (): React.ReactNode => <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleMenuKeyDown}>
|
||
|
|
<Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
|
||
|
|
|
||
|
|
<Box marginTop={1} flexDirection="column">
|
||
|
|
{menuItems.map((item, index_1) => <Text key={item.label} color={index_1 === selectedMenuIndex ? 'suggestion' : undefined}>
|
||
|
|
{index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '}
|
||
|
|
{item.label}
|
||
|
|
</Text>)}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{error && <Box marginTop={1}>
|
||
|
|
<Text color="error">{error}</Text>
|
||
|
|
</Box>}
|
||
|
|
</Box>;
|
||
|
|
switch (editMode) {
|
||
|
|
case 'menu':
|
||
|
|
return renderMenu();
|
||
|
|
case 'edit-tools':
|
||
|
|
return <ToolSelector tools={tools} initialTools={agent.tools} onComplete={async finalTools => {
|
||
|
|
setEditMode('menu');
|
||
|
|
await handleSave({
|
||
|
|
tools: finalTools
|
||
|
|
});
|
||
|
|
}} />;
|
||
|
|
case 'edit-color':
|
||
|
|
return <ColorPicker agentName={agent.agentType} currentColor={selectedColor || agent.color as AgentColorName || 'automatic'} onConfirm={async color => {
|
||
|
|
setSelectedColor(color);
|
||
|
|
setEditMode('menu');
|
||
|
|
await handleSave({
|
||
|
|
color
|
||
|
|
});
|
||
|
|
}} />;
|
||
|
|
case 'edit-model':
|
||
|
|
return <ModelSelector initialModel={agent.model} onComplete={async model => {
|
||
|
|
setEditMode('menu');
|
||
|
|
await handleSave({
|
||
|
|
model
|
||
|
|
});
|
||
|
|
}} />;
|
||
|
|
default:
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJjaGFsayIsImZpZ3VyZXMiLCJSZWFjdCIsInVzZUNhbGxiYWNrIiwidXNlTWVtbyIsInVzZVN0YXRlIiwidXNlU2V0QXBwU3RhdGUiLCJLZXlib2FyZEV2ZW50IiwiQm94IiwiVGV4dCIsInVzZUtleWJpbmRpbmciLCJUb29scyIsIkFnZW50Q29sb3JOYW1lIiwic2V0QWdlbnRDb2xvciIsIkFnZW50RGVmaW5pdGlvbiIsImdldEFjdGl2ZUFnZW50c0Zyb21MaXN0IiwiaXNDdXN0b21BZ2VudCIsImlzUGx1Z2luQWdlbnQiLCJlZGl0RmlsZUluRWRpdG9yIiwiZ2V0QWN0dWFsQWdlbnRGaWxlUGF0aCIsInVwZGF0ZUFnZW50RmlsZSIsIkNvbG9yUGlja2VyIiwiTW9kZWxTZWxlY3RvciIsIlRvb2xTZWxlY3RvciIsImdldEFnZW50U291cmNlRGlzcGxheU5hbWUiLCJQcm9wcyIsImFnZW50IiwidG9vbHMiLCJvblNhdmVkIiwibWVzc2FnZSIsIm9uQmFjayIsIkVkaXRNb2RlIiwiU2F2ZUNoYW5nZXMiLCJjb2xvciIsIm1vZGVsIiwiQWdlbnRFZGl0b3IiLCJSZWFjdE5vZGUiLCJzZXRBcHBTdGF0ZSIsImVkaXRNb2RlIiwic2V0RWRpdE1vZGUiLCJzZWxlY3RlZE1lbnVJbmRleCIsInNldFNlbGVjdGVkTWVudUluZGV4IiwiZXJyb3IiLCJzZXRFcnJvciIsInNlbGVjdGVkQ29sb3IiLCJzZXRTZWxlY3RlZENvbG9yIiwiaGFuZGxlT3BlbkluRWRpdG9yIiwiZmlsZVBhdGgiLCJyZXN1bHQiLCJhZ2VudFR5cGUiLCJoYW5kbGVTYXZlIiwiY2hhbmdlcyIsIm5ld1Rvb2xzIiwibmV3Q29sb3IiLCJuZXdNb2RlbCIsImZpbmFsQ29sb3IiLCJoYXNUb29sc0NoYW5nZWQiLCJ1bmRlZmluZWQiLCJoYXNNb2RlbENoYW5nZWQiLCJoYXNDb2xvckNoYW5nZWQiLCJ3aGVuVG9Vc2UiLCJnZXRTeXN0ZW1Qcm9tcHQiLCJzdGF0ZSIsImFsbEFnZW50cyIsImFnZW50RGVmaW5pdGlvbnMiLCJtYXAiLCJhIiwiYWN0aXZlQWdlbnRzIiwiYm9sZCIsImVyciIsIkVycm9yIiwibWVudUl0ZW1zIiwibGFiZWwiLCJhY3Rpb24iLCJoYW5kbGVFc2NhcGUiLCJoYW5kbGVNZW51S2V5RG93biIsImUiLCJrZXkiLCJwcmV2ZW50RGVmYXVsdCIsImluZGV4IiwiTWF0aCIsIm1heCIsIm1pbiIsImxlbmd0aCIsInNlbGVjdGVkSXRlbSIsImNvbnRleHQiLCJyZW5kZXJNZW51Iiwic291cmNlIiwiaXRlbSIsInBvaW50ZXIiLCJmaW5hbFRvb2xzIl0sInNvdXJjZXMiOlsiQWdlbnRFZGl0b3IudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBjaGFsayBmcm9tICdjaGFsaydcbmltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUNhbGxiYWNrLCB1c2VNZW1vLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlU2V0QXBwU3RhdGUgfSBmcm9tICdzcmMvc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7IEtleWJvYXJkRXZlbnQgfSBmcm9tICcuLi8uLi9pbmsvZXZlbnRzL2tleWJvYXJkLWV2ZW50LmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdXNlS2V5YmluZGluZyB9IGZyb20gJy4uLy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xzIH0gZnJvbSAnLi4vLi4vVG9vbC5qcydcbmltcG9ydCB7XG4gIHR5cGUgQWdlbnRDb2xvck5hbWUsXG4gIHNldEFnZW50Q29sb3IsXG59IGZyb20gJy4uLy4uL3Rvb2xzL0FnZW50VG9vbC9hZ2VudENvbG9yTWFuYWdlci5qcydcbmltcG9ydCB7XG4gIHR5cGUgQWdlbnREZWZpbml0aW9uLFxuICBnZXRBY3RpdmVBZ2VudHNGcm9tTGlzdCxcbiAgaXNDdXN0b21BZ2VudCxcbiAgaXNQbHVnaW5BZ2VudCxcbn0gZnJvbSAnLi4vLi4vdG9vbHMvQWdlbnRUb29sL2xvYWRBZ2VudHNEaXIuanMnXG5pbXBvcnQgeyBlZGl0RmlsZUluRWRpdG9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvcHJvbXB0RWRpdG9yLmpzJ1xuaW1wb3J0IHsgZ2V0QWN0dWFsQWdlbnRGaWxlUGF0aCwgdXBkYXRlQWdlbnRGaWxlIH0gZnJvbSAnLi9hZ2VudEZpbGVVdGlscy5qcydcbmltcG9ydCB7IENvbG9yUGlja2VyIH0gZnJvbSAnLi9Db2xvclBpY2tlci5qcydcbmltcG9ydCB7IE1vZGVsU2VsZWN0b3IgfSBmcm9tICcuL01vZGVsU2VsZWN0b3IuanMnXG5pbXBvcnQgeyBUb29sU2VsZWN0b3IgfSBmcm9tICcuL1Rvb2xTZWxlY3Rvci5qcydcbmltcG9ydCB7IGdldEFnZW50U291cmNlRGlzcGxheU5hbWUgfSBmcm9tICcuL3V0aWxzLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBhZ2VudDogQWdlbnREZWZpbml0aW9uXG4gIHRvb2xzOiBUb29sc1xuICBvblNhdmVkOiAobWVzc2FnZTogc3RyaW5nKSA9PiB2b2lkXG4gIG9uQmFjazogKCkgPT4gdm9pZFxufVxuXG50eXBlIEVkaXRNb2RlID0gJ21lbnUnIHwgJ2VkaXQtdG9vbHMnIHwgJ2VkaXQtY29sb3InIHwgJ2VkaXQtbW9kZWwnXG5cbnR5cGUgU2F2ZUNoYW5nZXMgPSB7XG4gIHRvb2xzPzogc3RyaW5nW11cbiAgY29sb3I/OiBBZ2VudENvbG9yTmFtZVxuICBtb2RlbD86IHN0cmluZ1xufVxuXG5leHBvcnQgZnVuY3Rpb24gQWdlbnRFZGl0b3Ioe1xuICBhZ2VudCxcbiAgdG9vbHMsXG4gIG9uU2F2ZWQsXG4gIG9uQmFjayxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3Qgc2V0QXBwU3RhdGUgPSB1c2VTZXRBcHBTdGF0ZSgpXG4gIGNvbnN0IFtlZGl0TW9kZSwgc2V0RWRpdE1vZGVdID0gdXNlU3RhdGU8RWRpdE1vZGU+KCdtZW51JylcbiAgY29uc3QgW3NlbGVjdGVkTWVudUluZGV4LCBzZXRTZWxlY3RlZE1lbnVJbmRleF0gPSB1c2VTdGF0ZSgwKVxuICBjb25zdCBbZXJyb3IsIHNldEVycm9yXSA9IHVzZVN0YXRlPHN0cmluZyB8IG51bGw+KG51bGwpXG4gIGNvbnN0IFtzZWxlY3RlZENvbG9yLCBzZXRTZWxlY3RlZENvbG9yXSA9IHVzZVN0YXRlPFxuICAgIEFnZW50Q29sb3JOYW1lIHwgdW5kZWZpbmVkXG4gID4oYWdlbnQuY29sb3IgYXMgQWdlbnRDb2xvck5hbWUgfCB1bmRlZmluZWQpXG5cbiAgY29uc3QgaGFuZGxlT3BlbkluRWRpdG9yID0
|