[Haik]: ckpt, view builder saving issue fixed via templates, system prompts, and skills. Also added a reset to default for modes.

This commit is contained in:
haikdc
2026-03-19 01:58:20 -07:00
parent 9943ccafd5
commit 6dcfb19b17
12 changed files with 461 additions and 99 deletions
+14 -36
View File
@@ -404,11 +404,6 @@ const AgentCard: React.FC<Props> = ({
}
};
const handleCollapse = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
dispatch(collapseSession(session.id));
};
useEffect(() => {
if (session.status === 'running' || session.status === 'waiting_approval') {
@@ -750,37 +745,20 @@ const AgentCard: React.FC<Props> = ({
onPointerDown={(e) => e.stopPropagation()}
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0, ml: 0.5 }}
>
{expanded ? (
<Tooltip title="Collapse">
<IconButton
size="small"
onClick={handleCollapse}
onMouseDown={(e) => e.stopPropagation()}
sx={{
color: c.text.ghost,
p: 0.5,
'&:hover': { color: c.text.secondary, bgcolor: c.bg.secondary },
}}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
) : (
<Tooltip title={isDraft ? 'Remove' : 'Close chat'}>
<IconButton
size="small"
onClick={handleRemove}
onMouseDown={(e) => e.stopPropagation()}
sx={{
color: c.text.ghost,
p: 0.5,
'&:hover': { color: c.status.error, bgcolor: `${c.status.errorBg}` },
}}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
)}
<Tooltip title={isDraft ? 'Remove' : 'Close chat'}>
<IconButton
size="small"
onClick={handleRemove}
onMouseDown={(e) => e.stopPropagation()}
sx={{
color: c.text.ghost,
p: 0.5,
'&:hover': { color: c.status.error, bgcolor: `${c.status.errorBg}` },
}}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
+82 -18
View File
@@ -27,6 +27,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import TuneIcon from '@mui/icons-material/Tune';
import LockIcon from '@mui/icons-material/Lock';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import RestoreIcon from '@mui/icons-material/Restore';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
@@ -37,6 +38,7 @@ import {
createMode,
updateMode,
deleteMode,
resetMode,
Mode,
} from '@/shared/state/modesSlice';
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
@@ -105,7 +107,7 @@ const ALL_BUILTIN_TOOL_NAMES = ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep',
const Modes: React.FC = () => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const { items, loading } = useAppSelector((s) => s.modes);
const { items, builtinDefaults, loading } = useAppSelector((s) => s.modes);
const toolItems = useAppSelector((s) => s.tools.items);
const modes = useMemo(() => Object.values(items), [items]);
@@ -174,6 +176,45 @@ const Modes: React.FC = () => {
await dispatch(deleteMode(id));
};
const editingIsBuiltin = editingId ? items[editingId]?.is_builtin ?? false : false;
const hasDiverged = useMemo(() => {
if (!editingId || !editingIsBuiltin) return false;
const defaults = builtinDefaults[editingId];
if (!defaults) return false;
const current = items[editingId];
if (!current) return false;
return (
current.name !== defaults.name ||
current.description !== defaults.description ||
(current.system_prompt ?? '') !== (defaults.system_prompt ?? '') ||
JSON.stringify(current.tools) !== JSON.stringify(defaults.tools) ||
(current.default_next_mode ?? '') !== (defaults.default_next_mode ?? '') ||
current.icon !== defaults.icon ||
current.color !== defaults.color ||
(current.default_folder ?? '') !== (defaults.default_folder ?? '')
);
}, [editingId, editingIsBuiltin, items, builtinDefaults]);
const handleReset = async () => {
if (!editingId) return;
const action = await dispatch(resetMode(editingId));
if (resetMode.fulfilled.match(action)) {
const m = action.payload;
setForm({
name: m.name,
description: m.description,
system_prompt: m.system_prompt ?? '',
tools: m.tools ?? [],
toolsEnabled: m.tools !== null,
default_next_mode: m.default_next_mode ?? '',
icon: m.icon,
color: m.color,
default_folder: m.default_folder ?? '',
});
}
};
const otherModes = modes.filter((m) => m.id !== editingId);
return (
@@ -505,23 +546,46 @@ const Modes: React.FC = () => {
</FormControl>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setDialogOpen(false)} sx={{ color: c.text.tertiary, textTransform: 'none' }}>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={!form.name}
sx={{
bgcolor: c.accent.primary,
'&:hover': { bgcolor: c.accent.pressed },
textTransform: 'none',
borderRadius: 2,
}}
>
{editingId ? 'Save Changes' : 'Create Mode'}
</Button>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between' }}>
<Box>
{editingIsBuiltin && (
<Tooltip title={hasDiverged ? 'Restore this mode to its original built-in defaults' : 'Mode matches built-in defaults'}>
<span>
<Button
startIcon={<RestoreIcon sx={{ fontSize: 16 }} />}
onClick={handleReset}
disabled={!hasDiverged}
sx={{
color: hasDiverged ? c.text.muted : c.text.ghost,
textTransform: 'none',
fontSize: '0.82rem',
'&:hover': hasDiverged ? { color: c.status.error, bgcolor: `${c.status.error}10` } : {},
}}
>
Reset to Default
</Button>
</span>
</Tooltip>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={() => setDialogOpen(false)} sx={{ color: c.text.tertiary, textTransform: 'none' }}>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={!form.name}
sx={{
bgcolor: c.accent.primary,
'&:hover': { bgcolor: c.accent.pressed },
textTransform: 'none',
borderRadius: 2,
}}
>
{editingId ? 'Save Changes' : 'Create Mode'}
</Button>
</Box>
</DialogActions>
</Dialog>
+3 -2
View File
@@ -381,7 +381,7 @@ interface FileTreeItemProps {
c: ReturnType<typeof useClaudeTokens>;
}
const PROTECTED_FILES = new Set(['index.html', 'schema.json', 'meta.json']);
const PROTECTED_FILES = new Set(['index.html', 'schema.json', 'meta.json', 'SKILL.md']);
const FileTreeItem: React.FC<FileTreeItemProps> = ({ node, depth, activeFile, onSelect, onDelete, c }) => {
const [open, setOpen] = useState(true);
@@ -732,6 +732,7 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
const outputFiles = { ...files };
delete outputFiles['meta.json'];
delete outputFiles['schema.json'];
delete outputFiles['SKILL.md'];
return {
name: name || 'Untitled App',
@@ -948,7 +949,7 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
? `${SERVE_BASE}/workspace/${workspaceId}/serve/index.html`
: undefined;
const filePaths = useMemo(() => Object.keys(files).filter(p => p !== 'meta.json').sort(), [files]);
const filePaths = useMemo(() => Object.keys(files).filter(p => p !== 'meta.json' && p !== 'SKILL.md').sort(), [files]);
const fileTree = useMemo(() => buildFileTree(filePaths), [filePaths]);
const updateFile = useCallback((path: string, content: string) => {
+15 -3
View File
@@ -18,18 +18,19 @@ export interface Mode {
interface ModesState {
items: Record<string, Mode>;
builtinDefaults: Record<string, Mode>;
loading: boolean;
loaded: boolean;
}
const initialState: ModesState = { items: {}, loading: false, loaded: false };
const initialState: ModesState = { items: {}, builtinDefaults: {}, loading: false, loaded: false };
export const fetchModes = createAsyncThunk(
'modes/fetch',
async () => {
const res = await fetch(`${MODES_API}/list`);
const data = await res.json();
return data.modes as Mode[];
return { modes: data.modes as Mode[], builtinDefaults: (data.builtin_defaults ?? {}) as Record<string, Mode> };
},
{ condition: (_, { getState }) => !(getState() as { modes: ModesState }).modes.loading },
);
@@ -60,6 +61,15 @@ export const updateMode = createAsyncThunk(
}
);
export const resetMode = createAsyncThunk(
'modes/reset',
async (id: string) => {
const res = await fetch(`${MODES_API}/${id}/reset`, { method: 'POST' });
const data = await res.json();
return data.mode as Mode;
}
);
export const deleteMode = createAsyncThunk('modes/delete', async (id: string) => {
await fetch(`${MODES_API}/${id}`, { method: 'DELETE' });
return id;
@@ -76,11 +86,13 @@ const modesSlice = createSlice({
state.loading = false;
state.loaded = true;
state.items = {};
for (const m of action.payload) state.items[m.id] = m;
for (const m of action.payload.modes) state.items[m.id] = m;
state.builtinDefaults = action.payload.builtinDefaults;
})
.addCase(fetchModes.rejected, (state) => { state.loading = false; state.loaded = true; })
.addCase(createMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(updateMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(resetMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(deleteMode.fulfilled, (state, action) => { delete state.items[action.payload]; });
},
});