miii-cli 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
- const MAX_FACTS = 200;
3
+ const MAX_FACTS = 50;
4
4
  function memoryPath(projectDir) {
5
5
  return join(projectDir, 'memory.json');
6
6
  }
@@ -28,6 +28,11 @@ const builtin = [
28
28
  return 'Normal mode.';
29
29
  },
30
30
  },
31
+ {
32
+ name: 'design',
33
+ ns: 'default',
34
+ description: 'Impeccable UI design — /design teach to learn brand, /design <task> to create',
35
+ },
31
36
  {
32
37
  name: 'review',
33
38
  ns: 'default',
@@ -39,7 +44,7 @@ const builtin = [
39
44
  ns: 'default',
40
45
  description: 'Show available commands',
41
46
  execute: (_, ctx) => {
42
- return 'Built-in: /review /mkdir /mv /touch /models /sessions /session /clear /list /help\nType /list for all loaded skills.';
47
+ return 'Built-in: /review /mkdir /mv /touch /models /sessions /session /clear /list /help\n\nDesign:\n /design teach answer 7 questions → generates DESIGN.md (impeccable design system)\n /design <task> implement UI using DESIGN.md as brand context\n\nType /list for all loaded skills.';
43
48
  },
44
49
  },
45
50
  {
@@ -7,6 +7,12 @@ import { getTavilyKey, tavilySearch, tavilyExtract } from '../tavily/client.js';
7
7
  const run = promisify(exec);
8
8
  const runFile = promisify(execFile);
9
9
  const EXEC_TIMEOUT_MS = 30_000;
10
+ function requireArg(val, name, tool) {
11
+ if (typeof val !== 'string' || !val.length) {
12
+ throw new Error(`${tool}: "${name}" argument is required and must be a non-empty string`);
13
+ }
14
+ return val;
15
+ }
10
16
  export const tools = [
11
17
  {
12
18
  name: 'read_file',
@@ -14,7 +20,7 @@ export const tools = [
14
20
  params: '{"path": "string"}',
15
21
  execute: async ({ path }) => {
16
22
  try {
17
- return readFile(guardPath(path));
23
+ return readFile(guardPath(requireArg(path, 'path', 'read_file')));
18
24
  }
19
25
  catch (e) {
20
26
  throw new Error(`read_file: ${e}`);
@@ -26,7 +32,7 @@ export const tools = [
26
32
  description: 'List directory contents',
27
33
  params: '{"path": "string", "recursive": "boolean (optional)"}',
28
34
  execute: async ({ path, recursive = false }) => {
29
- const entries = listFiles(guardPath(path), recursive);
35
+ const entries = listFiles(guardPath(requireArg(path, 'path', 'list_files')), recursive);
30
36
  if (!entries.length)
31
37
  return '(empty)';
32
38
  return entries.map(e => `${e.type === 'dir' ? 'd' : 'f'} ${e.rel}`).join('\n');
@@ -37,10 +43,10 @@ export const tools = [
37
43
  description: 'Create a new file with content — fails if file already exists. Prefer edit_file for new files.',
38
44
  params: '{"path": "string", "content": "string"}',
39
45
  execute: async ({ path, content }) => {
40
- const safe = guardPath(path);
46
+ const safe = guardPath(requireArg(path, 'path', 'create_file'));
41
47
  if (existsSync(safe))
42
48
  throw new Error(`file already exists: ${path}`);
43
- writeFile(safe, content);
49
+ writeFile(safe, requireArg(content, 'content', 'create_file'));
44
50
  return `created: ${path}`;
45
51
  },
46
52
  },
@@ -49,13 +55,13 @@ export const tools = [
49
55
  description: 'Write a new file — only for files that do not exist yet. Use update_file to modify existing files.',
50
56
  params: '{"path": "string", "content": "string"}',
51
57
  execute: async ({ path, content }) => {
52
- const safe = guardPath(path);
58
+ const safe = guardPath(requireArg(path, 'path', 'edit_file'));
53
59
  if (existsSync(safe)) {
54
60
  throw new Error(`edit_file cannot overwrite existing file: ${path}\n` +
55
61
  `Use update_file with <old> and <new> blocks to make targeted edits.\n` +
56
62
  `Call read_file first to get the exact current text.`);
57
63
  }
58
- const text = content;
64
+ const text = requireArg(content, 'content', 'edit_file');
59
65
  writeFile(safe, text);
60
66
  const lines = text.split('\n').length;
61
67
  return `created: ${path} (${lines} line${lines === 1 ? '' : 's'})`;
@@ -66,13 +72,15 @@ export const tools = [
66
72
  description: 'Replace an exact unique string in an existing file. Always call read_file first to get the exact text.',
67
73
  params: '{"path": "string", "old": "string", "new": "string"}',
68
74
  execute: async ({ path, old: oldStr, new: newStr }) => {
69
- const safe = guardPath(path);
75
+ const safe = guardPath(requireArg(path, 'path', 'update_file'));
70
76
  const current = readFile(safe);
71
77
  if (current === null)
72
78
  throw new Error(`file not found: ${path}`);
73
79
  if (current === '')
74
80
  throw new Error(`file empty: ${path}`);
75
- const old = oldStr;
81
+ const old = requireArg(oldStr, 'old', 'update_file');
82
+ if (newStr === undefined || newStr === null)
83
+ throw new Error('update_file: "new" argument is required');
76
84
  const count = current.split(old).length - 1;
77
85
  if (count === 0) {
78
86
  throw new Error(`old text not found in ${path} — file may have changed since last read.\n` +
@@ -104,7 +112,7 @@ export const tools = [
104
112
  description: 'Delete a file',
105
113
  params: '{"path": "string"}',
106
114
  execute: async ({ path }) => {
107
- deleteFile(guardPath(path));
115
+ deleteFile(guardPath(requireArg(path, 'path', 'delete_file')));
108
116
  return `deleted: ${path}`;
109
117
  },
110
118
  },
@@ -113,8 +121,9 @@ export const tools = [
113
121
  description: 'Run a shell command in cwd',
114
122
  params: '{"command": "string"}',
115
123
  execute: async ({ command }) => {
116
- const { stdout, stderr } = await run(command, { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS });
117
- return [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim();
124
+ const { stdout, stderr } = await run(requireArg(command, 'command', 'run_command'), { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS });
125
+ const out = [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim();
126
+ return out.length > 8000 ? out.slice(0, 8000) + '\n…[truncated]' : out;
118
127
  },
119
128
  },
120
129
  {
@@ -122,7 +131,7 @@ export const tools = [
122
131
  description: 'Create a directory (and any missing parents)',
123
132
  params: '{"path": "string"}',
124
133
  execute: async ({ path }) => {
125
- createDir(guardPath(path));
134
+ createDir(guardPath(requireArg(path, 'path', 'create_folder')));
126
135
  return `created: ${path}`;
127
136
  },
128
137
  },
@@ -131,7 +140,7 @@ export const tools = [
131
140
  description: 'Move or rename a file or directory',
132
141
  params: '{"from": "string", "to": "string"}',
133
142
  execute: async ({ from, to }) => {
134
- moveFile(guardPath(from), guardPath(to));
143
+ moveFile(guardPath(requireArg(from, 'from', 'move_file')), guardPath(requireArg(to, 'to', 'move_file')));
135
144
  return `moved: ${from} → ${to}`;
136
145
  },
137
146
  },
@@ -278,16 +287,14 @@ export function getSystemPrompt(extra = '', extraTools = []) {
278
287
  const toolDocs = allTools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
279
288
  const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool — gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first.
280
289
  - search_codebase({"query": "string", "k": "number (optional)"}): Semantic vector search over the indexed codebase. Returns top-k relevant code snippets by meaning. Requires the user to have run /index build. Use this when you need to find code by concept rather than exact string — e.g. "authentication logic", "error handling patterns", "database queries".`;
281
- return `You are Miii — a fast, local AI coding assistant.
290
+ return `You are Miii — AI coding assistant.
282
291
 
283
- Use tools by emitting:
292
+ Tools via:
284
293
  <tool_call>
285
294
  {"name": "tool_name", "args": {...}}
286
295
  </tool_call>
287
296
 
288
- Put file content in named blocks (never inside JSON — avoids escaping errors):
289
-
290
- For edit_file / create_file use <content> block:
297
+ File content in named blocks (not inside JSON):
291
298
  <tool_call>
292
299
  {"name": "edit_file", "args": {"path": "src/foo.ts"}}
293
300
  <content>
@@ -295,7 +302,6 @@ full file content here
295
302
  </content>
296
303
  </tool_call>
297
304
 
298
- For update_file use <old> and <new> blocks:
299
305
  <tool_call>
300
306
  {"name": "update_file", "args": {"path": "src/foo.ts"}}
301
307
  <old>
@@ -311,26 +317,11 @@ ${toolDocs}
311
317
  ${deepThinkDoc}
312
318
 
313
319
  Rules:
314
- - edit_file only works on NEW files it throws an error if the file exists. Never call it on existing files
315
- - To modify any existing file: call read_file first, then update_file with the exact text from that read as the <old> block
316
- - Never guess or reuse old text from earlier in the conversation always re-read immediately before patching
317
- - If update_file reports "old text not found", call read_file again and retry with the exact current text
318
- - Never delete without confirming
319
- - Use git_status and git_diff before any refactor to understand what has already changed
320
- - Use git_log to understand recent history before suggesting changes
321
- - Always call git_status before git_commit to verify what will be staged
322
- - Be concise
323
- - Output plain text only — never use markdown formatting in your responses
324
- - No headers (no #, ##), no bold (**text**), no italic (*text*), no bullet points with *, no horizontal rules (---)
325
- - NEVER show file content or code in your text response — always use edit_file, update_file, or create_file tools to write code to files
326
- - If you want to show the user code, write it to the file with a tool call instead
327
- - No fenced code blocks (no \`\`\`). If you find yourself about to write a code block, use a tool call instead
328
- - Use plain indentation and labels for structure. This is a terminal, not a chat UI
329
- - After editing files that have tests, call run_tests to verify nothing broke
330
- - If run_tests fails, read the failing test output and fix the code, then run_tests again (max 3 retries)
331
- - You have web_search and web_extract tools — use them whenever the user asks about anything requiring internet access, current information, documentation, library versions, news, or external URLs
332
- - NEVER say you cannot search the web — always call web_search instead
333
- - web_search REQUIRES the "query" key exactly — never omit it, never use "q" or "search_query" or any other key name
334
- - Use deep_think when the question requires gathering from multiple files or sources before you can answer well — it runs a safe read-only research phase and returns a summary you can reason over
335
- - deep_think cannot edit files or run shell commands — it is purely for information gathering${extra}`;
320
+ - edit_file: new files only (errors if exists). For existing files: read_file then update_file with exact <old> text
321
+ - Never guess old text always re-read immediately before patching. If "old text not found": read_file again and retry
322
+ - Plain text responses only. No markdown (#/*/\`), no code blockswrite code with tools, not in responses
323
+ - git_status/git_diff before refactors. git_status before git_commit
324
+ - run_tests after edits. Fix failures, retry up to 3 times
325
+ - web_search requires "query" key exactly. Never say you can't search always call web_search
326
+ - deep_think: read-only research only, cannot edit files${extra}`;
336
327
  }
@@ -1,12 +1,15 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useRef, useMemo, useEffect } from 'react';
2
+ import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
3
5
  import { Box, Text, useStdout } from 'ink';
4
6
  import { InputArea } from './components/InputArea.js';
5
7
  import { ModelPicker } from './components/ModelPicker.js';
6
8
  import { ConfigPicker } from './components/ConfigPicker.js';
7
9
  import { Divider } from './components/StatusBar.js';
10
+ import { DesignTeachModal, DESIGN_TEACH_QUESTIONS, buildDesignPrompt } from './components/DesignTeachModal.js';
8
11
  import { tools } from '../tools/index.js';
9
- import { toolArgSummary, formatElapsed } from './printer.js';
12
+ import { toolLabel, permissionDesc, formatElapsed, EDIT_TOOLS, DELETE_TOOLS } from './printer.js';
10
13
  import { MacroQueue } from '../tasks/queue.js';
11
14
  import { TaskExecutor } from '../tasks/executor.js';
12
15
  import { THINKING_PHRASES, SPARKLE } from './thinking.js';
@@ -96,7 +99,26 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
96
99
  const executorRef = useRef(new TaskExecutor(tools));
97
100
  const lastGitStatusRef = useRef('');
98
101
  const abortRef = useRef(null);
99
- const { projectDir, setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, renameFromMessage, updateMemory, } = useSession(session, cwd, config, mcpTools);
102
+ const [designTeachState, setDesignTeachState] = useState(null);
103
+ const [designReadyPrompt, setDesignReadyPrompt] = useState(null);
104
+ const { projectDir, setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, setHistory, buildContext, renameFromMessage, updateMemory, } = useSession(session, cwd, config, mcpTools);
105
+ const startDesignTeach = useCallback(() => {
106
+ setDesignTeachState({ answers: [], idx: 0 });
107
+ }, []);
108
+ const handleDesignAnswer = useCallback((answer) => {
109
+ setDesignTeachState(prev => {
110
+ if (!prev)
111
+ return null;
112
+ const answers = [...prev.answers, answer];
113
+ const nextIdx = prev.idx + 1;
114
+ if (nextIdx >= DESIGN_TEACH_QUESTIONS.length) {
115
+ const exists = existsSync(join(cwd, 'DESIGN.md'));
116
+ setDesignReadyPrompt(buildDesignPrompt(DESIGN_TEACH_QUESTIONS, answers, exists));
117
+ return null;
118
+ }
119
+ return { answers, idx: nextIdx };
120
+ });
121
+ }, []);
100
122
  const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, handleModelSelect, handleModelPull, } = useModelPicker(config);
101
123
  const deepThinkTool = useMemo(() => ({
102
124
  name: 'deep_think',
@@ -109,7 +131,7 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
109
131
  }), [config]);
110
132
  const searchTool = useMemo(() => createSearchCodebaseTool(config, cwd), [config, cwd]);
111
133
  const allTools = useMemo(() => [...tools, deepThinkTool, searchTool, ...mcpTools], [deepThinkTool, searchTool, mcpTools]);
112
- const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, compactRequest, resolveCompact, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
134
+ const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef, setHistory);
113
135
  const { runRefactor } = useRefactor({
114
136
  config, currentModelRef, systemPromptRef, abortRef,
115
137
  macroQueueRef, executorRef,
@@ -126,7 +148,15 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
126
148
  runRefactor, handleGit, lastGitStatusRef, mcpTools, setConfig,
127
149
  setConfigOpen, updateMemory,
128
150
  startWatch, stopWatch, watchActive,
151
+ startDesignTeach,
129
152
  });
153
+ useEffect(() => {
154
+ if (!designReadyPrompt)
155
+ return;
156
+ setDesignReadyPrompt(null);
157
+ pushHistory({ role: 'user', content: designReadyPrompt });
158
+ runLoop(buildContext(), 0, 'create or update DESIGN.md');
159
+ }, [designReadyPrompt, pushHistory, buildContext, runLoop]);
130
160
  const skillList = skills.list();
131
161
  return (_jsxs(Box, { flexDirection: "column", children: [configOpen ? (_jsxs(_Fragment, { children: [_jsx(ConfigPicker, { config: config, currentModel: currentModel, tavilyKey: tavilyKey, onUpdate: ({ model, ...configPatch }) => {
132
162
  if (model)
@@ -135,7 +165,11 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
135
165
  setConfig(c => ({ ...c, ...configPatch }));
136
166
  saveConfig(configPatch);
137
167
  }
138
- }, onTavilyKey: (key) => { saveTavilyKey(key); setTavilyKey(key); }, onClose: () => { setConfigOpen(false); } }), _jsx(Divider, { cols: cols })] })) : pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : compactRequest ? (_jsxs(Box, { paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: "context is large" }), _jsxs(Text, { color: "gray", children: ["(~", compactRequest.messageCount, "k chars)"] })] }), _jsx(Text, { color: "gray", dimColor: true, children: "compact to keep responses fast, or keep full history" })] })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: permissionRequest.toolName }), _jsx(Text, { color: "gray", children: toolArgSummary(permissionRequest.args) })] }), _jsx(DiffPreview, { toolName: permissionRequest.toolName, args: permissionRequest.args })] })) : (status === 'thinking' || status === 'tool') ? (_jsx(Box, { paddingX: 1, gap: 1, children: status === 'thinking'
168
+ }, onTavilyKey: (key) => { saveTavilyKey(key); setTavilyKey(key); }, onClose: () => { setConfigOpen(false); } }), _jsx(Divider, { cols: cols })] })) : pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : designTeachState ? (_jsx(DesignTeachModal, { question: DESIGN_TEACH_QUESTIONS[designTeachState.idx], index: designTeachState.idx, total: DESIGN_TEACH_QUESTIONS.length })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DELETE_TOOLS.has(permissionRequest.toolName) ? 'red' : EDIT_TOOLS.has(permissionRequest.toolName) ? 'green' : 'blue', children: "\u25CF" }), _jsx(Text, { color: "white", bold: true, children: toolLabel(permissionRequest.toolName, permissionRequest.args) })] }), _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "gray", dimColor: true, children: ['└ ', permissionDesc(permissionRequest.toolName)] }) }), _jsx(DiffPreview, { toolName: permissionRequest.toolName, args: permissionRequest.args })] })) : (status === 'thinking' || status === 'tool') ? (_jsx(Box, { paddingX: 1, gap: 1, children: status === 'thinking'
139
169
  ? _jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: SPARKLE[tick % SPARKLE.length] }), _jsx(Text, { color: Math.floor(tick / 4) % 6 >= 2 && Math.floor(tick / 4) % 6 <= 4 ? 'white' : 'gray', italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] }), _jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })
140
- : _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }), _jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] }) })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, compactRequest: compactRequest, onCompactResponse: resolveCompact, onSubmit: handleSubmit, onAbort: handleAbort, history: historyRef.current.filter(m => m.role === 'user').map(m => m.content), watchActive: watchActive })] }));
170
+ : _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }), _jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] }) })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, designTeach: designTeachState ? {
171
+ question: DESIGN_TEACH_QUESTIONS[designTeachState.idx],
172
+ index: designTeachState.idx,
173
+ total: DESIGN_TEACH_QUESTIONS.length,
174
+ } : null, onDesignTeachAnswer: handleDesignAnswer, onSubmit: handleSubmit, onAbort: handleAbort, history: historyRef.current.filter(m => m.role === 'user').map(m => m.content), watchActive: watchActive })] }));
141
175
  }
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export const DESIGN_TEACH_QUESTIONS = [
4
+ 'What does your product do? (one sentence)',
5
+ 'Who are your primary users? (e.g. developers, consumers, small teams, enterprises)',
6
+ 'How should it feel? List 3–5 words (e.g. bold, calm, precise, playful, minimal, trustworthy)',
7
+ 'Any existing brand colors? (hex codes — or "none" to start fresh)',
8
+ 'Any existing fonts? (font names — or "none" to choose new ones)',
9
+ 'Interface type? (dashboard / marketing / app / docs / landing page / other)',
10
+ 'Products or brands you want to look DIFFERENT from? (or "none")',
11
+ ];
12
+ export function DesignTeachModal({ question, index, total }) {
13
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u25C6 design setup" }), _jsxs(Text, { color: "gray", dimColor: true, children: [index + 1, " of ", total] })] }), _jsx(Text, { color: "white", children: question })] }));
14
+ }
15
+ export function buildDesignPrompt(questions, answers, exists) {
16
+ const brief = questions.map((q, i) => `${q}\n→ ${answers[i] ?? '(skipped)'}`).join('\n\n');
17
+ const fileInstruction = exists
18
+ ? `DESIGN.md already exists. First call read_file on DESIGN.md to get the current content, then call update_file to replace the entire content with the updated design system (use the full current file as the <old> block).`
19
+ : `Write DESIGN.md to the project root using the create_file tool.`;
20
+ return `You are an expert UI/UX designer following impeccable design principles. ${exists ? 'Update' : 'Create'} DESIGN.md based on this product brief.
21
+
22
+ Product Brief:
23
+ ${brief}
24
+
25
+ Impeccable design philosophy (apply strictly):
26
+ - Typography: choose purposeful, distinctive fonts — avoid Inter/Roboto/generic defaults. Use modular scale. Consider display fonts for headings if brand personality supports it.
27
+ - Color: OKLCH-based system, tint neutrals with primary hue, minimum 4.5:1 contrast. No generic gray-on-white. Define primary, secondary, accent, 5 neutral steps, semantic colors (success/warning/error/info).
28
+ - Spatial: 4px-base spacing scale, clear hierarchy through deliberate whitespace.
29
+ - Motion: cubic-bezier curves (not linear/ease), respect prefers-reduced-motion.
30
+ - Interaction: always-visible focus states, meaningful hover/active transitions.
31
+ - Anti-patterns to eliminate: nested card shadows, purple-to-blue gradients, Inter everywhere, insufficient contrast, centered walls of text, auto-playing anything, generic SaaS aesthetics.
32
+
33
+ ${exists ? 'Update' : 'Create'} DESIGN.md with ALL these sections:
34
+
35
+ ## Product
36
+ [2-3 sentences: what it is, who uses it, core value]
37
+
38
+ ## Brand Voice
39
+ [5 personality adjectives + 1 sentence on visual tone]
40
+
41
+ ## Colors
42
+ Full OKLCH color system with hex equivalents:
43
+ - Primary: oklch(...) / #... [usage: CTAs, links, key actions]
44
+ - Secondary: oklch(...) / #...
45
+ - Accent: oklch(...) / #...
46
+ - Neutral-50 through Neutral-900 (tinted with primary hue)
47
+ - Success / Warning / Error / Info
48
+ Rationale: why these colors fit the brand personality.
49
+
50
+ ## Typography
51
+ Heading font: [specific name] — why it fits this brand
52
+ Body font: [specific name] — why it fits
53
+ Scale:
54
+ - 3xl: 2.5rem / 700 — hero headings
55
+ - 2xl: 2rem / 600 — section headings
56
+ - xl: 1.5rem / 600 — card titles
57
+ - lg: 1.125rem / 500 — subheadings
58
+ - base: 1rem / 400 — body text
59
+ - sm: 0.875rem / 400 — captions, labels
60
+ - xs: 0.75rem / 400 — metadata
61
+
62
+ ## Spacing
63
+ Token scale (4px base):
64
+ space-1: 4px | space-2: 8px | space-3: 12px | space-4: 16px
65
+ space-6: 24px | space-8: 32px | space-12: 48px | space-16: 64px | space-24: 96px
66
+
67
+ ## Components
68
+ - Border-radius approach and why (sharp/medium/large/pill)
69
+ - Shadow style (none/subtle/elevated/dramatic)
70
+ - Button: primary, secondary, ghost — hover/active/focus/disabled states
71
+ - Input: default, focus, error, disabled states
72
+ - Card: background, border, shadow, padding
73
+ - Navigation: desktop + mobile pattern
74
+
75
+ ## Motion
76
+ - Durations: fast 100ms / base 200ms / slow 350ms
77
+ - Easing: cubic-bezier(0.4, 0, 0.2, 1) standard, cubic-bezier(0, 0, 0.2, 1) decelerate
78
+ - Use cases: button click, hover transitions, modal open, page change
79
+
80
+ ## Anti-patterns
81
+ 8–10 specific things NOT to do for THIS product (derive from brand personality + interface type, not generic advice)
82
+
83
+ ## Design Principles
84
+ 4 guiding principles specific to this product — not generic platitudes
85
+
86
+ Be specific, opinionated, brand-appropriate. Every choice needs a reason. No placeholder text.
87
+ ${fileInstruction}`;
88
+ }
@@ -56,7 +56,7 @@ function wordEndAfter(line, col) {
56
56
  i++;
57
57
  return i;
58
58
  }
59
- export function InputArea({ status, skills, cwd, planningMode, permissionRequest, onPermissionResponse, compactRequest, onCompactResponse, onSubmit, onAbort, history = [], watchActive = false }) {
59
+ export function InputArea({ status, skills, cwd, planningMode, permissionRequest, onPermissionResponse, designTeach, onDesignTeachAnswer, onSubmit, onAbort, history = [], watchActive = false }) {
60
60
  const [lines, setLines] = useState(['']);
61
61
  const [cursor, setCursor] = useState({ row: 0, col: 0 });
62
62
  const [overlay, setOverlay] = useState('none');
@@ -180,19 +180,19 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
180
180
  onSubmit(name);
181
181
  }
182
182
  function selectFile(file) {
183
+ const r = cursor.row;
184
+ const line = lines[r];
185
+ const before = line.slice(0, cursor.col);
186
+ const atIdx = before.lastIndexOf('@');
187
+ if (atIdx === -1)
188
+ return;
189
+ const newLine = line.slice(0, atIdx) + '@' + file.rel + ' ' + line.slice(cursor.col);
183
190
  setLines(prev => {
184
191
  const next = [...prev];
185
- const r = cursor.row;
186
- const line = next[r];
187
- const before = line.slice(0, cursor.col);
188
- const atIdx = before.lastIndexOf('@');
189
- if (atIdx === -1)
190
- return prev;
191
- const newLine = line.slice(0, atIdx) + '@' + file.rel + ' ' + line.slice(cursor.col);
192
192
  next[r] = newLine;
193
- setCursor({ row: r, col: atIdx + 1 + file.rel.length + 1 });
194
193
  return next;
195
194
  });
195
+ setCursor({ row: r, col: atIdx + 1 + file.rel.length + 1 });
196
196
  setOverlay('none');
197
197
  setOverlayIdx(0);
198
198
  }
@@ -212,17 +212,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
212
212
  }
213
213
  return;
214
214
  }
215
- if (compactRequest && onCompactResponse) {
216
- if (input === 'y' || input === 'Y') {
217
- onCompactResponse(true);
218
- return;
219
- }
220
- if (input === 'n' || input === 'N' || key.escape) {
221
- onCompactResponse(false);
222
- return;
223
- }
224
- return;
225
- }
226
215
  if (key.escape) {
227
216
  if (overlay !== 'none') {
228
217
  setOverlay('none');
@@ -277,6 +266,12 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
277
266
  }
278
267
  }
279
268
  if (key.return) {
269
+ if (designTeach && onDesignTeachAnswer) {
270
+ const answer = fullInput.trim();
271
+ clearInput();
272
+ onDesignTeachAnswer(answer || '(skipped)');
273
+ return;
274
+ }
280
275
  const typed = fullInput.trim();
281
276
  const pasted = pasteRef.current;
282
277
  const text = pasted
@@ -433,14 +428,20 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
433
428
  appendChar(input);
434
429
  if (prospective.startsWith('/')) {
435
430
  if (prospective.slice(1).includes(' ')) {
436
- setOverlay('none');
431
+ if (input === '@' || overlay === 'at') {
432
+ setOverlay('at');
433
+ setOverlayIdx(0);
434
+ }
435
+ else {
436
+ setOverlay('none');
437
+ }
437
438
  }
438
439
  else {
439
440
  setOverlay('command');
440
441
  setOverlayIdx(0);
441
442
  }
442
443
  }
443
- else if (input === '@' || (overlay === 'at' && atQuery !== undefined)) {
444
+ else if (input === '@' || (overlay === 'at' && atQuery !== '')) {
444
445
  setOverlay('at');
445
446
  setOverlayIdx(0);
446
447
  }
@@ -453,10 +454,10 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
453
454
  const cols = stdout.columns ?? 80;
454
455
  const availWidth = Math.max(20, cols - 4); // paddingX(2) + "> "(2)
455
456
  const isProcessing = status !== 'idle';
456
- const promptColor = (permissionRequest || compactRequest) ? 'yellow' : isProcessing ? 'yellow' : 'green';
457
+ const promptColor = permissionRequest ? 'yellow' : designTeach ? 'cyan' : isProcessing ? 'yellow' : 'green';
457
458
  const inHistory = historyIdx !== -1;
458
- const hint = compactRequest
459
- ? 'y compact n keep full context'
459
+ const hint = designTeach
460
+ ? 'enter submit answer esc skip'
460
461
  : permissionRequest
461
462
  ? 'y approve once a approve for session n deny'
462
463
  : isProcessing
@@ -475,7 +476,7 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
475
476
  const pastePreview = pasteRef.current
476
477
  ? pasteRef.current.split('\n')[0].slice(0, cols - 6)
477
478
  : '';
478
- return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 3, children: [_jsx(Text, { color: "green", bold: true, children: "y once" }), _jsx(Text, { color: "cyan", bold: true, children: "a session" }), _jsx(Text, { color: "red", bold: true, children: "n deny" })] })) : compactRequest ? (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", bold: true, children: "y yes" }), _jsx(Text, { color: "red", bold: true, children: "n no" })] })) : pasteLines > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " line", pasteLines !== 1 ? 's' : ''] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] }), pastePreview && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", pastePreview, pasteRef.current.split('\n')[0].length > cols - 6 ? '…' : ''] }))] })) : lines.length === 1 && !lines[0] ? (watchActive && isActive
479
+ return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 3, children: [_jsx(Text, { color: "green", bold: true, children: "y once" }), _jsx(Text, { color: "cyan", bold: true, children: "a session" }), _jsx(Text, { color: "red", bold: true, children: "n deny" })] })) : pasteLines > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " line", pasteLines !== 1 ? 's' : ''] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] }), pastePreview && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", pastePreview, pasteRef.current.split('\n')[0].length > cols - 6 ? '…' : ''] }))] })) : lines.length === 1 && !lines[0] ? (watchActive && isActive
479
480
  ? _jsxs(Text, { children: [_jsx(Text, { color: "cyan", dimColor: true, children: "watching\u2026 " }), _jsx(Text, { children: "\u2588" })] })
480
481
  : _jsx(Text, { children: isActive ? '█' : ' ' })) : (lines.map((line, i) => (_jsx(Text, { children: i === cursor.row
481
482
  ? viewportLine(line, cursor.col, availWidth, isActive)
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
3
3
  import { chat } from '../../llm/stream.js';
4
4
  import { tools as staticTools } from '../../tools/index.js';
5
5
  import { StreamParser, extractBareToolCall } from '../../parser/stream-parser.js';
6
- import { shouldCompact, compactContext, contextSize } from '../../tasks/compactor.js';
6
+ import { shouldCompact, compactContext } from '../../tasks/compactor.js';
7
7
  import * as printer from '../printer.js';
8
8
  const MAX_TOOL_DEPTH = 10;
9
9
  const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'update_file', 'delete_file']);
@@ -11,36 +11,31 @@ const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
11
11
  const PERMISSION_TOOLS = new Set(['edit_file', 'update_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
12
12
  const CHECKPOINT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'delete_file']);
13
13
  // Tool result messages that are ephemeral — never worth storing in memory or compact summaries
14
- const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[current state of|^\[Context compacted/;
14
+ const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[current state of|^\[Context compacted|^\[file updated:/;
15
15
  export function stripEphemeral(messages) {
16
16
  return messages.filter(m => m.role !== 'user' || !EPHEMERAL_PATTERN.test(m.content));
17
17
  }
18
- export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef) {
18
+ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef, replaceHistory) {
19
19
  const [status, setStatus] = useState('idle');
20
20
  const [tick, setTick] = useState(0);
21
21
  const [currentTool, setCurrentTool] = useState();
22
22
  const [taskLabel, setTaskLabel] = useState();
23
23
  const [permissionRequest, setPermissionRequest] = useState(null);
24
24
  const permissionResolveRef = useRef(null);
25
- const [compactRequest, setCompactRequest] = useState(null);
26
- const compactResolveRef = useRef(null);
27
25
  const checkpointRef = useRef(new Map());
28
26
  const sessionApprovedRef = useRef(new Set());
29
27
  const thinkingStartRef = useRef(0);
30
28
  const extraToolsRef = useRef(extraTools);
31
29
  extraToolsRef.current = extraTools;
32
30
  const pushHistoryRef = useRef(pushHistory);
31
+ const replaceHistoryRef = useRef(replaceHistory);
33
32
  useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
33
+ useEffect(() => { replaceHistoryRef.current = replaceHistory; }, [replaceHistory]);
34
34
  const resolvePermission = useCallback((result) => {
35
35
  permissionResolveRef.current?.(result);
36
36
  permissionResolveRef.current = null;
37
37
  setPermissionRequest(null);
38
38
  }, []);
39
- const resolveCompact = useCallback((approved) => {
40
- compactResolveRef.current?.(approved);
41
- compactResolveRef.current = null;
42
- setCompactRequest(null);
43
- }, []);
44
39
  useEffect(() => {
45
40
  if (status === 'idle')
46
41
  return;
@@ -60,24 +55,16 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
60
55
  }
61
56
  let msgs = contextMsgs;
62
57
  if (shouldCompact(contextMsgs)) {
63
- const approved = await new Promise(resolve => {
64
- compactResolveRef.current = resolve;
65
- setCompactRequest({ messageCount: Math.round(contextSize(contextMsgs) / 1000) });
66
- });
67
- if (approved) {
68
- printer.systemMsg('compacting context…');
69
- const toCompact = stripEphemeral(contextMsgs);
70
- msgs = await compactContext(toCompact, {
71
- provider: config.provider,
72
- model: currentModelRef.current,
73
- baseUrl: config.baseUrl,
74
- apiKey: config.apiKey,
75
- }, goal);
76
- printer.systemMsg(`compacted: ${contextMsgs.length} → ${msgs.length} messages`);
77
- }
78
- else {
79
- printer.systemMsg('keeping full context — responses may be slower');
80
- }
58
+ printer.systemMsg('compacting context…');
59
+ const toCompact = stripEphemeral(contextMsgs);
60
+ msgs = await compactContext(toCompact, {
61
+ provider: config.provider,
62
+ model: currentModelRef.current,
63
+ baseUrl: config.baseUrl,
64
+ apiKey: config.apiKey,
65
+ }, goal);
66
+ printer.systemMsg(`compacted: ${contextMsgs.length} → ${msgs.length} messages`);
67
+ replaceHistoryRef.current?.(msgs.filter(m => m.role !== 'system'));
81
68
  }
82
69
  abortRef.current = new AbortController();
83
70
  await chat({
@@ -176,13 +163,13 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
176
163
  if (occurrences === 0) {
177
164
  printer.errorMsg(`patch stale: old text not found in ${filePath} — injecting fresh content`);
178
165
  next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
179
- next.push({ role: 'user', content: `update_file failed: old text not found in ${filePath}. The file content above is the current state. Retry update_file with the correct exact text.` });
166
+ next.push({ role: 'user', content: `update_file failed: the <old> text you used does not exist in ${filePath}. The CURRENT file content is shown above. Re-read it carefully, find the exact text you want to replace, and retry update_file using text that exactly matches what is in the file now.` });
180
167
  continue;
181
168
  }
182
169
  if (occurrences > 1) {
183
170
  printer.errorMsg(`patch ambiguous: old text matches ${occurrences} locations in ${filePath} — injecting fresh content`);
184
171
  next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
185
- next.push({ role: 'user', content: `update_file failed: old text matches ${occurrences} locations in ${filePath}. Use more surrounding context to make old text unique, then retry.` });
172
+ next.push({ role: 'user', content: `update_file failed: the <old> text matches ${occurrences} locations in ${filePath}. Add more surrounding lines to the <old> block to make it unique, then retry.` });
186
173
  continue;
187
174
  }
188
175
  }
@@ -193,12 +180,11 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
193
180
  if (SHOW_RESULT_TOOLS.has(tc.name))
194
181
  printer.toolMsg(tc.name, result);
195
182
  next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
196
- // After any file edit, inject fresh file state so next tool sees actual content
197
183
  if (FILE_EDIT_TOOLS.has(tc.name)) {
198
184
  const filePath = tc.args.path;
199
185
  if (filePath && existsSync(filePath)) {
200
- const fresh = readFileSync(filePath, 'utf-8');
201
- next.push({ role: 'user', content: `[current state of ${filePath} after edit]\n${fresh}` });
186
+ const lineCount = readFileSync(filePath, 'utf-8').split('\n').length;
187
+ next.push({ role: 'user', content: `[file updated: ${filePath} ${lineCount} lines]` });
202
188
  }
203
189
  }
204
190
  }
@@ -223,7 +209,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
223
209
  if (didEditFiles) {
224
210
  const systemMsg = msgs.find(m => m.role === 'system');
225
211
  const goalMsg = msgs.find(m => m.role === 'user' && !m.content.startsWith('[') && !m.content.startsWith('Tool '));
226
- const batchStart = msgs.length + 1; // index in next where this batch's messages start
212
+ const batchStart = msgs.length; // include assistant message so model sees its own tool call on retry
227
213
  const batchMsgs = next.slice(batchStart);
228
214
  const slimCtx = [
229
215
  ...(systemMsg ? [systemMsg] : []),
@@ -251,11 +237,6 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
251
237
  permissionResolveRef.current = null;
252
238
  setPermissionRequest(null);
253
239
  }
254
- if (compactResolveRef.current) {
255
- compactResolveRef.current(false);
256
- compactResolveRef.current = null;
257
- setCompactRequest(null);
258
- }
259
240
  // Restore checkpointed files
260
241
  if (checkpointRef.current.size > 0) {
261
242
  let restored = 0;
@@ -285,6 +266,5 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
285
266
  thinkingStartRef,
286
267
  runLoop, handleAbort,
287
268
  permissionRequest, resolvePermission,
288
- compactRequest, resolveCompact,
289
269
  };
290
270
  }
@@ -5,7 +5,7 @@ import { getTavilyKey, saveTavilyKey } from '../../tavily/client.js';
5
5
  import * as printer from '../printer.js';
6
6
  import { loadLongMemory, saveLongMemory, mergeFacts, formatMemoryBlock } from '../../memory/store.js';
7
7
  import { extractFacts } from '../../memory/extractor.js';
8
- const SHORT_MEMORY_SIZE = 40;
8
+ const SHORT_MEMORY_SIZE = 50;
9
9
  function buildSystemPrompt(cwd, facts, extraTools = []) {
10
10
  return getSystemPrompt(`\n- CWD: ${cwd}`, extraTools) + formatMemoryBlock(facts);
11
11
  }
@@ -88,6 +88,10 @@ export function useSession(initialSession, cwd, config, extraTools = []) {
88
88
  ctx.push(extra);
89
89
  return ctx;
90
90
  }
91
+ function setHistory(msgs) {
92
+ historyRef.current = msgs;
93
+ scheduleSave();
94
+ }
91
95
  function updateMemory(newFacts) {
92
96
  if (!newFacts.length)
93
97
  return;
@@ -101,6 +105,6 @@ export function useSession(initialSession, cwd, config, extraTools = []) {
101
105
  sessionName, setSessionName, sessionNameRef,
102
106
  historyRef, saveTimerRef, systemPromptRef,
103
107
  longMemoryRef,
104
- pushHistory, buildContext, renameFromMessage, updateMemory,
108
+ pushHistory, setHistory, buildContext, renameFromMessage, updateMemory,
105
109
  };
106
110
  }
@@ -37,7 +37,7 @@ export function useSubmit(deps) {
37
37
  const depsRef = useRef(deps);
38
38
  depsRef.current = deps;
39
39
  const handleSubmit = useCallback(async (text) => {
40
- const { config, skills, cwd, projectDir, version, currentModelRef, setCurrentModel, historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef, setPlanningMode, runLoop, buildContext, pushHistory, setSessionName, renameFromMessage, setStatus, setTaskLabel, setCurrentTool, runRefactor, handleGit, lastGitStatusRef, mcpTools, setConfig, setConfigOpen, updateMemory, startWatch, stopWatch, } = depsRef.current;
40
+ const { config, skills, cwd, projectDir, version, currentModelRef, setCurrentModel, historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef, setPlanningMode, runLoop, buildContext, pushHistory, setSessionName, renameFromMessage, setStatus, setTaskLabel, setCurrentTool, runRefactor, handleGit, lastGitStatusRef, mcpTools, setConfig, setConfigOpen, updateMemory, startWatch, stopWatch, startDesignTeach, } = depsRef.current;
41
41
  const cmd = text.trim();
42
42
  if (cmd === '?') {
43
43
  printer.systemMsg('shortcuts:\n' +
@@ -52,7 +52,10 @@ export function useSubmit(deps) {
52
52
  ' @filename inject file into context\n' +
53
53
  ' /cmd open command palette\n' +
54
54
  ' esc abort / clear input\n' +
55
- ' ctrl+c abort / exit');
55
+ ' ctrl+c abort / exit\n' +
56
+ '\ndesign commands:\n' +
57
+ ' /design teach answer 7 questions → generates DESIGN.md (impeccable system)\n' +
58
+ ' /design <task> design or implement UI using DESIGN.md as brand context');
56
59
  return;
57
60
  }
58
61
  if (cmd === '/version') {
@@ -241,16 +244,60 @@ export function useSubmit(deps) {
241
244
  printer.systemMsg('usage: /refactor <goal>');
242
245
  return;
243
246
  }
247
+ const atRefactorContext = buildAtContext(text);
248
+ if (atRefactorContext)
249
+ pushHistory({ role: 'user', content: atRefactorContext });
244
250
  await runRefactor(goal);
245
251
  return;
246
252
  }
253
+ if (cmd === '/design' || cmd.startsWith('/design ')) {
254
+ const sub = cmd.slice(7).trim();
255
+ if (!sub || sub === 'teach') {
256
+ startDesignTeach();
257
+ return;
258
+ }
259
+ // Design task phase: use DESIGN.md context + impeccable principles
260
+ const atDesignContext = buildAtContext(text);
261
+ let designContext = '';
262
+ try {
263
+ const designFile = readFile(guardPath('DESIGN.md', cwd));
264
+ if (designFile)
265
+ designContext = `\n\nProject design system (from DESIGN.md):\n${designFile}\n`;
266
+ }
267
+ catch { }
268
+ const impeccableRules = `
269
+ Impeccable design rules — follow strictly:
270
+ - Typography: purposeful font selection, modular scale, intentional pairing. No Inter/Roboto by default.
271
+ - Color: OKLCH-based system, tinted neutrals, 4.5:1 contrast minimum. No generic gray-on-white.
272
+ - Spatial design: consistent spacing scale, clear visual hierarchy, intentional whitespace.
273
+ - Motion: contemporary easing (cubic-bezier not linear), respect prefers-reduced-motion.
274
+ - Interaction: visible focus states, loading patterns, meaningful hover states.
275
+ - Responsive: mobile-first, fluid typography where appropriate.
276
+ - UX copy: precise microcopy in labels, errors, empty states. No lorem ipsum.
277
+ - Anti-patterns to eliminate: nested card shadows, purple-to-blue gradients, disabled gray without reason, centered walls of text, auto-playing anything.
278
+ - Write distinctive, crafted UI — not generic SaaS templates.
279
+ - Write all code to files using tools. No code blocks in responses.`;
280
+ const taskDesc = sub.replace(/@[\w./\-]+/g, '').trim();
281
+ const taskPrompt = `${designContext}${impeccableRules}
282
+
283
+ Design task: ${taskDesc}
284
+
285
+ Analyze what exists, then implement the design. Use the design system above if available. Make it distinctive and well-crafted.`;
286
+ printer.userMsg(text);
287
+ pushHistory({ role: 'user', content: atDesignContext + taskPrompt });
288
+ await runLoop(buildContext(), 0, sub);
289
+ return;
290
+ }
247
291
  if (cmd.startsWith('/think ') || cmd === '/think') {
248
292
  const query = cmd.slice(6).trim();
249
293
  if (!query) {
250
294
  printer.systemMsg('usage: /think <query>');
251
295
  return;
252
296
  }
253
- printer.userMsg(`/think ${query}`);
297
+ const atThinkContext = buildAtContext(text);
298
+ if (atThinkContext)
299
+ pushHistory({ role: 'user', content: atThinkContext });
300
+ printer.userMsg(text);
254
301
  setStatus('thinking');
255
302
  setTaskLabel(`gathering: ${query}`);
256
303
  abortRef.current = new AbortController();
@@ -473,6 +520,9 @@ export function useSubmit(deps) {
473
520
  return;
474
521
  }
475
522
  if (skill.execute) {
523
+ const atContext = buildAtContext(text);
524
+ if (atContext)
525
+ pushHistory({ role: 'user', content: atContext });
476
526
  const ctx = {
477
527
  messages: historyRef.current.map(m => ({ role: m.role, content: m.content })),
478
528
  appendMessage: (_role, content) => printer.systemMsg(content),
@@ -485,8 +535,9 @@ export function useSubmit(deps) {
485
535
  return;
486
536
  }
487
537
  if (skill.prompt) {
488
- printer.userMsg(skill.prompt);
489
- pushHistory({ role: 'user', content: skill.prompt });
538
+ const atContext = buildAtContext(text);
539
+ printer.userMsg(text);
540
+ pushHistory({ role: 'user', content: atContext + skill.prompt });
490
541
  await runLoop(buildContext());
491
542
  return;
492
543
  }
@@ -25,6 +25,8 @@ const gray = (s) => col(90, s);
25
25
  const yellow = (s) => col(93, s);
26
26
  const purple = (s) => col(95, s);
27
27
  const red = (s) => col(91, s);
28
+ function bgRed(s) { return `\x1b[48;2;65;18;18m\x1b[91m${s}${R}`; }
29
+ function bgGreen(s) { return `\x1b[48;2;14;46;14m\x1b[92m${s}${R}`; }
28
30
  function stripMarkdown(s) {
29
31
  return s
30
32
  .replace(/\*\*\*(.+?)\*\*\*/g, '$1')
@@ -121,6 +123,7 @@ export function welcome(provider, model, cwd, version, updateAvailable, linked)
121
123
  ` ${bold(yellow('Tips for getting started'))}`,
122
124
  ` Type ${cyan('@filename')} to inject file into context`,
123
125
  ` Use ${cyan('/skill')} to run a skill or command`,
126
+ ` Use ${cyan('/design teach')} to generate a design system`,
124
127
  ` Use ${cyan('/config')} to switch provider, model, or API key`,
125
128
  '',
126
129
  ];
@@ -159,30 +162,42 @@ export function assistantMsg(text) {
159
162
  const tail = lines.slice(idx + 1).join('\n');
160
163
  write(`\n${blue('●')} ${head}${tail ? '\n' + tail : ''}\n`);
161
164
  }
162
- const EDIT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'write_file']);
163
- const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
164
- function toolLabel(name, args) {
165
+ export const EDIT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'write_file']);
166
+ export const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
167
+ const PERM_DESC = {
168
+ delete_file: 'delete this file',
169
+ update_file: 'edit this file',
170
+ create_file: 'create this file',
171
+ edit_file: 'create this file',
172
+ move_file: 'move this file',
173
+ run_command: 'run in shell',
174
+ git_commit: 'commit to git',
175
+ };
176
+ export function permissionDesc(toolName) {
177
+ return PERM_DESC[toolName] ?? 'allow this action';
178
+ }
179
+ export function toolLabel(name, args) {
165
180
  const a = args;
166
181
  const short = (s, n = 55) => s.length > n ? s.slice(0, n) + '…' : s;
167
182
  switch (name) {
168
- case 'read_file': return `Reading ${a.path ?? ''}`;
169
- case 'list_files': return `Listing ${a.path || '.'}`;
170
- case 'create_file': return `Creating ${a.path ?? ''}`;
171
- case 'edit_file': return `Writing ${a.path ?? ''}`;
172
- case 'update_file': return `Updating ${a.path ?? ''}`;
173
- case 'delete_file': return `Deleting ${a.path ?? ''}`;
174
- case 'move_file': return `Moving ${a.from} → ${a.to}`;
175
- case 'create_folder': return `Creating folder ${a.path ?? ''}`;
176
- case 'run_command': return `Running ${short(a.command ?? '')}`;
177
- case 'git_status': return 'Checking git status';
178
- case 'git_diff': return 'Reading diff';
179
- case 'git_log': return 'Reading commits';
180
- case 'git_commit': return `Committing: ${short(a.message ?? '')}`;
181
- case 'run_tests': return a.path ? `Running tests › ${a.path}` : 'Running tests';
182
- case 'web_search': return `Searching: ${short(a.query ?? '')}`;
183
- case 'web_extract': return `Extracting page`;
184
- case 'deep_think': return `Researching: ${short(a.query ?? '')}`;
185
- case 'search_codebase': return `Searching codebase: ${short(a.query ?? '')}`;
183
+ case 'read_file': return `Read(${a.path ?? ''})`;
184
+ case 'list_files': return `List(${a.path || '.'})`;
185
+ case 'create_file': return `Create(${a.path ?? ''})`;
186
+ case 'edit_file': return `Create(${a.path ?? ''})`;
187
+ case 'update_file': return `Update(${a.path ?? ''})`;
188
+ case 'delete_file': return `Delete(${a.path ?? ''})`;
189
+ case 'move_file': return `Move(${a.from} → ${a.to})`;
190
+ case 'create_folder': return `Mkdir(${a.path ?? ''})`;
191
+ case 'run_command': return `Run(${short(a.command ?? '')})`;
192
+ case 'git_status': return 'Git(status)';
193
+ case 'git_diff': return 'Git(diff)';
194
+ case 'git_log': return 'Git(log)';
195
+ case 'git_commit': return `Git(commit: ${short(a.message ?? '')})`;
196
+ case 'run_tests': return a.path ? `Test(${a.path})` : 'Test(suite)';
197
+ case 'web_search': return `Search(${short(a.query ?? '')})`;
198
+ case 'web_extract': return `Extract(${Array.isArray(a.urls) ? String(a.urls[0] ?? '') : String(a.urls ?? 'url')})`;
199
+ case 'deep_think': return `Think(${short(a.query ?? '')})`;
200
+ case 'search_codebase': return `Index(${short(a.query ?? '')})`;
186
201
  default: {
187
202
  const s = toolArgSummary(args);
188
203
  return s ? `${name} ${s}` : name;
@@ -192,20 +207,31 @@ function toolLabel(name, args) {
192
207
  export function planSummary(tools) {
193
208
  if (!tools.length)
194
209
  return;
195
- const header = gray(`─ plan (${tools.length} action${tools.length === 1 ? '' : 's'})`);
196
- write(header + '\n');
210
+ const lines = [gray(`─ plan (${tools.length} action${tools.length === 1 ? '' : 's'})`)];
197
211
  for (const t of tools) {
198
212
  const dot = DELETE_TOOLS.has(t.name) ? red('◦') : EDIT_TOOLS.has(t.name) ? green('◦') : blue('◦');
199
- const label = toolLabel(t.name, t.args);
200
- write(` ${dot} ${gray(label)}\n`);
213
+ lines.push(` ${dot} ${gray(toolLabel(t.name, t.args))}`);
201
214
  }
215
+ write(lines.join('\n') + '\n');
202
216
  }
203
217
  const DIFF_CTX = 2;
204
218
  const DIFF_MAX = 40;
205
219
  function printUpdateDiff(filePath, oldText, newText) {
206
220
  const oldLines = oldText.split('\n');
207
221
  const newLines = newText.split('\n');
208
- write(gray(` └ Added ${newLines.length} line${newLines.length !== 1 ? 's' : ''}, removed ${oldLines.length} line${oldLines.length !== 1 ? 's' : ''}\n`));
222
+ const addedCount = newLines.length;
223
+ const removedCount = oldLines.length;
224
+ const parts = [];
225
+ if (addedCount > 0 && removedCount > 0) {
226
+ parts.push(green(`+${addedCount}`), gray(' / '), red(`-${removedCount}`));
227
+ }
228
+ else if (addedCount > 0) {
229
+ parts.push(green(`+${addedCount} line${addedCount !== 1 ? 's' : ''}`));
230
+ }
231
+ else {
232
+ parts.push(red(`-${removedCount} line${removedCount !== 1 ? 's' : ''}`));
233
+ }
234
+ const out = [` ${gray('└')} ${parts.join('')}\n`];
209
235
  let fileLines = [];
210
236
  let lineOffset = 0;
211
237
  try {
@@ -216,47 +242,50 @@ function printUpdateDiff(filePath, oldText, newText) {
216
242
  if (idx >= 0)
217
243
  lineOffset = content.slice(0, idx).split('\n').length - 1;
218
244
  else
219
- fileLines = []; // old text not in file — skip context lines
245
+ fileLines = [];
220
246
  }
221
247
  }
222
248
  catch { }
223
249
  let shown = 0;
224
250
  const ctxStart = Math.max(0, lineOffset - DIFF_CTX);
225
251
  for (let i = ctxStart; i < lineOffset && shown < DIFF_MAX; i++, shown++) {
226
- write(gray(` ${String(i + 1).padStart(4)} ${fileLines[i] ?? ''}\n`));
252
+ out.push(` ${gray(String(i + 1).padStart(4))} ${gray(fileLines[i] ?? '')}\n`);
227
253
  }
228
254
  for (let i = 0; i < oldLines.length && shown < DIFF_MAX; i++, shown++) {
229
- write(` ${gray(String(lineOffset + i + 1).padStart(4))} ${red('- ')}${red(oldLines[i])}\n`);
255
+ out.push(` ${gray(String(lineOffset + i + 1).padStart(4))} ${bgRed(`- ${oldLines[i]}`)}\n`);
230
256
  }
231
257
  for (let i = 0; i < newLines.length && shown < DIFF_MAX; i++, shown++) {
232
- write(` ${gray(String(lineOffset + i + 1).padStart(4))} ${green('+ ')}${green(newLines[i])}\n`);
258
+ out.push(` ${gray(String(lineOffset + i + 1).padStart(4))} ${bgGreen(`+ ${newLines[i]}`)}\n`);
233
259
  }
234
260
  const ctxEnd = Math.min(fileLines.length, lineOffset + oldLines.length + DIFF_CTX);
235
261
  for (let i = lineOffset + oldLines.length; i < ctxEnd && shown < DIFF_MAX; i++, shown++) {
236
- write(gray(` ${String(i + 1).padStart(4)} ${fileLines[i] ?? ''}\n`));
262
+ out.push(` ${gray(String(i + 1).padStart(4))} ${gray(fileLines[i] ?? '')}\n`);
237
263
  }
264
+ return out.join('');
238
265
  }
239
266
  function printEditPreview(content) {
240
267
  const lines = content.split('\n');
241
268
  const visible = lines.slice(0, DIFF_MAX);
242
269
  const hidden = lines.length - visible.length;
243
- write(gray(` └ ${lines.length} line${lines.length !== 1 ? 's' : ''}\n`));
270
+ const out = [` ${gray('')} ${green(`+${lines.length} line${lines.length !== 1 ? 's' : ''}`)}\n`];
244
271
  visible.forEach((line, i) => {
245
- write(` ${gray(String(i + 1).padStart(4))} ${green('+ ')}${green(line)}\n`);
272
+ out.push(` ${gray(String(i + 1).padStart(4))} ${bgGreen(`+ ${line}`)}\n`);
246
273
  });
247
274
  if (hidden > 0)
248
- write(gray(` …${hidden} more line${hidden !== 1 ? 's' : ''}\n`));
275
+ out.push(gray(` …${hidden} more line${hidden !== 1 ? 's' : ''}\n`));
276
+ return out.join('');
249
277
  }
250
278
  export function toolCallStart(name, args) {
251
279
  const dot = DELETE_TOOLS.has(name) ? red('●') : EDIT_TOOLS.has(name) ? green('●') : blue('●');
252
- write(`\n${dot} ${bold(toolLabel(name, args))}\n`);
280
+ let out = `\n${dot} ${bold(toolLabel(name, args))}\n`;
253
281
  const a = args;
254
282
  if (name === 'update_file' && a.old && a.new && a.path) {
255
- printUpdateDiff(a.path, a.old, a.new);
283
+ out += printUpdateDiff(a.path, a.old, a.new);
256
284
  }
257
- else if (name === 'edit_file' && a.content && a.path) {
258
- printEditPreview(a.content);
285
+ else if ((name === 'edit_file' || name === 'create_file') && a.content && a.path) {
286
+ out += printEditPreview(a.content);
259
287
  }
288
+ write(out);
260
289
  }
261
290
  export function toolResultSummary(name, args, result) {
262
291
  const a = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
6
6
  "license": "MIT",