miii-cli 1.2.2 → 1.2.3

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
  {
@@ -114,7 +114,8 @@ export const tools = [
114
114
  params: '{"command": "string"}',
115
115
  execute: async ({ command }) => {
116
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();
117
+ const out = [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim();
118
+ return out.length > 8000 ? out.slice(0, 8000) + '\n…[truncated]' : out;
118
119
  },
119
120
  },
120
121
  {
@@ -278,16 +279,14 @@ export function getSystemPrompt(extra = '', extraTools = []) {
278
279
  const toolDocs = allTools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
279
280
  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
281
  - 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.
282
+ return `You are Miii — AI coding assistant.
282
283
 
283
- Use tools by emitting:
284
+ Tools via:
284
285
  <tool_call>
285
286
  {"name": "tool_name", "args": {...}}
286
287
  </tool_call>
287
288
 
288
- Put file content in named blocks (never inside JSON — avoids escaping errors):
289
-
290
- For edit_file / create_file use <content> block:
289
+ File content in named blocks (not inside JSON):
291
290
  <tool_call>
292
291
  {"name": "edit_file", "args": {"path": "src/foo.ts"}}
293
292
  <content>
@@ -295,7 +294,6 @@ full file content here
295
294
  </content>
296
295
  </tool_call>
297
296
 
298
- For update_file use <old> and <new> blocks:
299
297
  <tool_call>
300
298
  {"name": "update_file", "args": {"path": "src/foo.ts"}}
301
299
  <old>
@@ -311,26 +309,11 @@ ${toolDocs}
311
309
  ${deepThinkDoc}
312
310
 
313
311
  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}`;
312
+ - edit_file: new files only (errors if exists). For existing files: read_file then update_file with exact <old> text
313
+ - Never guess old text always re-read immediately before patching. If "old text not found": read_file again and retry
314
+ - Plain text responses only. No markdown (#/*/\`), no code blockswrite code with tools, not in responses
315
+ - git_status/git_diff before refactors. git_status before git_commit
316
+ - run_tests after edits. Fix failures, retry up to 3 times
317
+ - web_search requires "query" key exactly. Never say you can't search always call web_search
318
+ - deep_think: read-only research only, cannot edit files${extra}`;
336
319
  }
@@ -1,10 +1,13 @@
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
12
  import { toolArgSummary, formatElapsed } from './printer.js';
10
13
  import { MacroQueue } from '../tasks/queue.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", 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'
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');
@@ -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
@@ -453,10 +448,10 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
453
448
  const cols = stdout.columns ?? 80;
454
449
  const availWidth = Math.max(20, cols - 4); // paddingX(2) + "> "(2)
455
450
  const isProcessing = status !== 'idle';
456
- const promptColor = (permissionRequest || compactRequest) ? 'yellow' : isProcessing ? 'yellow' : 'green';
451
+ const promptColor = permissionRequest ? 'yellow' : designTeach ? 'cyan' : isProcessing ? 'yellow' : 'green';
457
452
  const inHistory = historyIdx !== -1;
458
- const hint = compactRequest
459
- ? 'y compact n keep full context'
453
+ const hint = designTeach
454
+ ? 'enter submit answer esc skip'
460
455
  : permissionRequest
461
456
  ? 'y approve once a approve for session n deny'
462
457
  : isProcessing
@@ -475,7 +470,7 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
475
470
  const pastePreview = pasteRef.current
476
471
  ? pasteRef.current.split('\n')[0].slice(0, cols - 6)
477
472
  : '';
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
473
+ 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
474
  ? _jsxs(Text, { children: [_jsx(Text, { color: "cyan", dimColor: true, children: "watching\u2026 " }), _jsx(Text, { children: "\u2588" })] })
480
475
  : _jsx(Text, { children: isActive ? '█' : ' ' })) : (lines.map((line, i) => (_jsx(Text, { children: i === cursor.row
481
476
  ? 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']);
@@ -15,32 +15,27 @@ const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[cur
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({
@@ -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
  }
@@ -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') {
@@ -244,6 +247,42 @@ export function useSubmit(deps) {
244
247
  await runRefactor(goal);
245
248
  return;
246
249
  }
250
+ if (cmd === '/design' || cmd.startsWith('/design ')) {
251
+ const sub = cmd.slice(7).trim();
252
+ if (!sub || sub === 'teach') {
253
+ startDesignTeach();
254
+ return;
255
+ }
256
+ // Design task phase: use DESIGN.md context + impeccable principles
257
+ let designContext = '';
258
+ try {
259
+ const designFile = readFile(guardPath('DESIGN.md', cwd));
260
+ if (designFile)
261
+ designContext = `\n\nProject design system (from DESIGN.md):\n${designFile}\n`;
262
+ }
263
+ catch { }
264
+ const impeccableRules = `
265
+ Impeccable design rules — follow strictly:
266
+ - Typography: purposeful font selection, modular scale, intentional pairing. No Inter/Roboto by default.
267
+ - Color: OKLCH-based system, tinted neutrals, 4.5:1 contrast minimum. No generic gray-on-white.
268
+ - Spatial design: consistent spacing scale, clear visual hierarchy, intentional whitespace.
269
+ - Motion: contemporary easing (cubic-bezier not linear), respect prefers-reduced-motion.
270
+ - Interaction: visible focus states, loading patterns, meaningful hover states.
271
+ - Responsive: mobile-first, fluid typography where appropriate.
272
+ - UX copy: precise microcopy in labels, errors, empty states. No lorem ipsum.
273
+ - Anti-patterns to eliminate: nested card shadows, purple-to-blue gradients, disabled gray without reason, centered walls of text, auto-playing anything.
274
+ - Write distinctive, crafted UI — not generic SaaS templates.
275
+ - Write all code to files using tools. No code blocks in responses.`;
276
+ const taskPrompt = `${designContext}${impeccableRules}
277
+
278
+ Design task: ${sub}
279
+
280
+ Analyze what exists, then implement the design. Use the design system above if available. Make it distinctive and well-crafted.`;
281
+ printer.userMsg(`/design ${sub}`);
282
+ pushHistory({ role: 'user', content: taskPrompt });
283
+ await runLoop(buildContext(), 0, sub);
284
+ return;
285
+ }
247
286
  if (cmd.startsWith('/think ') || cmd === '/think') {
248
287
  const query = cmd.slice(6).trim();
249
288
  if (!query) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
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",