miii-cli 0.3.1 โ†’ 0.3.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # ๐Ÿš€ Miii CLI โ€” High-Performance Local AI Coding Agent
1
+ # Miii โ€” The High-Performance Local AI Coding Agent
2
2
 
3
- **The definitive local AI coding agent for your terminal. Automate complex engineering workflows with total control, zero cloud, and zero Python overhead.**
3
+ > **You're paying $200/month for an AI that reads your private code and sends it to a cloud server you don't control. There's a better way.**
4
4
 
5
5
  ![MIII Demo](mii-cli.gif)
6
6
 
@@ -9,90 +9,166 @@
9
9
  [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
10
10
  [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
11
11
 
12
- ## ๐Ÿ“Š The Competitive Edge
12
+ ---
13
13
 
14
- | Feature | **Miii** | Claude Code | Codex CLI | Aider |
15
- |---|---|---|---|---|
16
- | **Execution Environment** | โœ… Local / Hybrid | โŒ Cloud only | โŒ Cloud only | โœ… Local + cloud |
17
- | **Data Privacy** | โœ… Air-gapped possible | โŒ Cloud-streamed | โŒ Cloud-streamed | โš ๏ธ Model-dependent |
18
- | **Cost Structure** | ๐Ÿ†“ Free (Your Compute) | ๐Ÿ’ณ Token-based | ๐Ÿ’ณ Token-based | ๐Ÿ†“ Free (local) |
19
- | **Runtime Efficiency** | โšก TS (Instant Start) | ๐Ÿ Node (Fast) | ๐Ÿ Node | ๐Ÿข Python (Heavy) |
20
- | **Research Engine** | โœ… Deep Think Mode | โŒ | โŒ | โŒ |
21
- | **Validation Loop** | โœ… Auto-test (Jest/Vitest) | โš ๏ธ Manual | โŒ | โš ๏ธ Manual |
22
- | **Live Web Access** | โœ… Tavily Integrated | โŒ | โŒ | โŒ |
23
- | **Edit Precision** | โœ… Surgical `patch_file` | โœ… | โš ๏ธ | โœ… |
24
- | **State Persistence** | โœ… Named Sessions | โœ… | โŒ | โš ๏ธ Basic |
25
- | **Extensibility** | โœ… npm + `.md` Skills | โš ๏ธ MCP only | โŒ | โŒ |
26
- | **License** | โœ… MIT | โŒ | โŒ | โœ… Apache 2.0 |
14
+ **Miii is a fully autonomous coding agent that runs entirely on your machine.** It plans, edits files, runs your tests, searches the web, indexes your codebase semantically, and iterates until the job is done โ€” all without a single byte of your code leaving your network.
27
15
 
28
- > โœ… = Native  |  โš ๏ธ = Partial  |  โŒ = Unsupported
16
+ Zero subscription. Zero cloud dependency. Zero Python overhead. **176 KB total.** Just raw engineering horsepower in your terminal.
29
17
 
30
- ## โšก๏ธ Quick Start
18
+ ```bash
19
+ npm install -g miii-cli && miii
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Why Engineers Are Switching
25
+
26
+ Claude Code is impressive. It's also a 50 MB binary that costs $200/month, requires an internet connection, and sends every line of your codebase to a server you don't own.
27
+
28
+ **Miii does everything Claude Code does. It's 176 KB. It's free. It runs on your laptop.**
29
+
30
+ GitHub Copilot streams your proprietary code to Microsoft. Aider is a Python monolith that takes longer to boot than to write a function. All of them charge you monthly for the privilege of being the product.
31
+
32
+ Miii flips the model. Your compute. Your data. Your rules.
33
+
34
+ ---
35
+
36
+ ## What Miii Actually Does
37
+
38
+ This isn't a fancy autocomplete. Miii is a **full autonomous agent loop:**
39
+
40
+ 1. You describe a goal
41
+ 2. Miii reads your codebase, plans the changes, edits the files
42
+ 3. It runs your test suite automatically after every change
43
+ 4. If tests fail, it reads the error, fixes the code, re-runs
44
+ 5. It repeats until the work is done
45
+
46
+ No babysitting. No copy-pasting error messages. No broken half-edits.
47
+
48
+ ---
49
+
50
+ ## What a Session Looks Like
51
+
52
+ ```
53
+ > refactor the auth module to use JWT instead of sessions
54
+
55
+ โ— Researching: refactor auth module to use JWT
56
+ โ— Reading src/auth/session.ts
57
+ โ— Reading src/middleware/auth.ts
58
+ โ— Reading src/routes/login.ts
59
+
60
+ Planning: 3 file(s) to change
61
+
62
+ โ— Editing src/auth/session.ts
63
+ โ— Editing src/middleware/auth.ts
64
+ โ— Editing src/routes/login.ts
65
+ โ— Running tests
66
+
67
+ โ”€ refactor done โ€” 3 file(s) processed
68
+ ```
69
+
70
+ No prompts asking which files to change. No copy-pasting error messages. Just: describe the goal, watch it work.
31
71
 
32
- Deploy Miii in your environment in 30 seconds:
72
+ ---
73
+
74
+ ## Killer Features
75
+
76
+ **๐Ÿ” Semantic Codebase Indexing** *(new in v0.3.2)*
77
+ Build a vector index of your entire codebase using local embeddings. Ask "where is the auth logic?" and Miii finds it by meaning, not keyword. No data leaves your machine.
78
+
79
+ **๐Ÿง  Deep Think Engine**
80
+ Before answering complex questions, Miii runs a constrained research phase โ€” reading files, checking git history, searching the web โ€” then synthesizes a grounded answer. Not a hallucination. A conclusion.
81
+
82
+ **๐ŸŒ Real-Time Web Access**
83
+ Tavily-powered web search and page extraction, built in. Ask about breaking changes in a library you just upgraded. Get an answer that's actually current.
84
+
85
+ **๐Ÿ›  Surgical File Editing**
86
+ `patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. No token waste. Exactly the change, nothing more.
87
+
88
+ **๐Ÿ”„ Self-Healing Test Loop**
89
+ Miii runs `npm test` after every file change. If something breaks, it reads the failure trace and fixes it autonomously โ€” up to 3 retries before surfacing the issue to you.
90
+
91
+ **๐Ÿ“‚ Persistent Sessions**
92
+ Pick up exactly where you left off. Named sessions mean your context, your history, and your goal survive terminal restarts.
93
+
94
+ **๐Ÿ“ฆ Skill System**
95
+ Extend Miii with plain Markdown files or npm packages. Ship reusable agent behaviors as versioned packages your whole team can pull.
96
+
97
+ ---
98
+
99
+ ## The Numbers That Matter
100
+
101
+ | | **Miii** | Claude Code | Aider |
102
+ |---|:---:|:---:|:---:|
103
+ | Monthly cost | **$0** | $20โ€“200 | $0 |
104
+ | Bundle size | **176 KB** | ~50 MB | ~200 MB |
105
+ | Your code stays local | **โœ…** | โŒ | โš ๏ธ |
106
+ | Startup time | **<100ms** | ~2s | ~4s |
107
+ | Semantic codebase index | **โœ…** | โŒ | โŒ |
108
+ | Deep research mode | **โœ…** | โŒ | โŒ |
109
+ | Auto test loop | **โœ…** | โš ๏ธ | โš ๏ธ |
110
+ | Works air-gapped | **โœ…** | โŒ | โŒ |
111
+ | License | **MIT** | Proprietary | Apache 2.0 |
112
+
113
+ ---
114
+
115
+ ## Get Running in 60 Seconds
33
116
 
34
117
  ```bash
35
- # 1. Pull a capable local model
118
+ # 1. Start Ollama and pull a model
36
119
  ollama pull qwen2.5-coder:7b
37
120
 
38
- # 2. Install the CLI globally
121
+ # 2. Install Miii
39
122
  npm install -g miii-cli
40
123
 
41
- # 3. Start engineering
124
+ # 3. Go to your project and start
125
+ cd your-project
42
126
  miii
43
127
  ```
44
128
 
45
- ## ๐Ÿง  Why Miii?
46
-
47
- The industry is saturated with heavy Python wrappers and expensive monthly subscriptions that trade your intellectual property for convenience. **Miii breaks this cycle.**
129
+ That's it. No API keys. No account. No sign-up form.
48
130
 
49
- - **Privacy by Default**: Your codebase never leaves your machine. Period.
50
- - **Zero Latency**: Built with TypeScript for near-instant startup. No virtual environments, no dependency hell, just raw performance.
51
- - **True Autonomy**: Miii isn't a chatbot; it's a junior engineer. It plans, edits, runs tests, and iterates until the PR is ready.
52
- - **Architectural Intelligence**: By analyzing git diffs and project structure, Miii understands context without requiring manual copy-pasting.
131
+ ---
53
132
 
54
- ## ๐Ÿ”ฅ Killer Features
133
+ ## Power Commands
55
134
 
56
- - **๐Ÿ›  Surgical Precision**: Instead of overwriting files, Miii uses `patch_file` to inject changes, preserving your formatting and reducing token waste.
57
- - **๐Ÿ”„ The Self-Healing Loop**: Miii executes your test suite (Jest, Vitest, Mocha) after every change. If a test fails, it analyzes the trace and fixes the code autonomously.
58
- - **๐ŸŒ Real-time Intelligence**: Integrated `web_search` and `web_extract` via Tavily allow Miii to reference the latest documentation and API changes.
59
- - **๐Ÿง  Deep Think Engine**: A sophisticated two-phase research mode that gathers data before synthesizing a solution.
60
- - **๐Ÿ“ Strategic Planning**: Use `/plan` to map out complex refactors before a single character is typed.
61
- - **๐Ÿ“‚ Persistent Context**: Workflows are saved as named sessions. Jump back into a specific feature branch with `miii --session feature-auth`.
62
- - **๐Ÿ“ฆ Modular Skill System**: Extend Miii's capabilities using npm plugins or simple Markdown-based skill files.
135
+ | Command | What it does |
136
+ |---|---|
137
+ | `/think <question>` | Deep research: reads files + web, then answers |
138
+ | `/refactor <goal>` | Autonomous multi-file refactor with test validation |
139
+ | `/index build` | Build semantic vector index of your codebase |
140
+ | `/index search <query>` | Find code by meaning, not string match |
141
+ | `/git review` | AI reviews your current diff for bugs and issues |
142
+ | `/git commit <msg>` | Stage everything and commit in one shot |
143
+ | `/plan <topic>` | Structured planning mode before you write a line |
144
+ | `/model <name>` | Hot-swap your LLM mid-conversation |
145
+ | `/session <name>` | Switch between named project sessions |
146
+ | `@filename` | Inject any file directly into context |
63
147
 
64
- ## ๐Ÿ”ฌ Deep Think Explained
148
+ ---
65
149
 
66
- Deep Think is a recursive research engine designed to eliminate "hallucinations" by grounding the AI in facts:
150
+ ## Semantic Codebase Indexing
67
151
 
68
- 1. **Gather Phase**: A constrained, read-only loop utilizing `read_file`, `list_files`, `git_status`, `git_log`, `git_diff`, and web tools.
69
- - **Guardrails**: Strict limit of 6 tool calls and 4 web calls to prevent infinite loops.
70
- - **Safety**: Zero write permissions. No mutations.
71
- 2. **Synthesize Phase**: All gathered intelligence is aggregated and fed into the main execution loop for a grounded, verified response.
152
+ For large codebases, Miii can build and query a local vector index โ€” no third-party APIs, no embeddings sent anywhere.
72
153
 
73
- **Trigger Research:**
74
154
  ```bash
75
- /think "How does the auth middleware handle token expiry?"
76
- /think "Analyze the project structure and explain the data flow."
77
- /think "What are the breaking changes in React 19 for this project?"
155
+ # Pull an embedding model (one time)
156
+ ollama pull nomic-embed-text
157
+
158
+ # Index your project
159
+ /index build
160
+
161
+ # The agent now calls search_codebase automatically
162
+ # when it needs to find code by concept
78
163
  ```
79
- *The LLM also triggers `deep_think` autonomously when it detects a high-complexity query.*
80
164
 
81
- ## โŒจ๏ธ Command Cheat Sheet
165
+ The agent calls `search_codebase` on its own when needed. You don't have to think about it.
82
166
 
83
- | Command | Purpose | Impact |
84
- |---|---|---|
85
- | `/think <query>` | Deep Research | High-fidelity synthesis of files + web |
86
- | `/refactor <goal>` | Full-scale Engineering | Plan $\rightarrow$ Edit $\rightarrow$ Test loop |
87
- | `/git <sub>` | Git Automation | Instant status, diffs, and AI commit messages |
88
- | `/plan` | Architecture Mode | Structured blueprinting before coding |
89
- | `/model <name>` | LLM Hot-swap | Switch models instantly based on task |
90
- | `/tavily-key <key>` | Enable Web Access | Unlocks real-time internet browsing |
91
- | `/sessions` | Context Recovery | Resume previous engineering sessions |
167
+ ---
92
168
 
93
- ## โš™๏ธ Configuration
169
+ ## Configuration
94
170
 
95
- Fine-tune your agent in `.miii.json` or `~/.config/miii/config.json`:
171
+ Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
96
172
 
97
173
  ```json
98
174
  {
@@ -100,25 +176,36 @@ Fine-tune your agent in `.miii.json` or `~/.config/miii/config.json`:
100
176
  "provider": "ollama",
101
177
  "baseUrl": "http://localhost:11434",
102
178
  "gitContext": true,
103
- "tavilyApiKey": "tvly-..."
179
+ "tavilyApiKey": "tvly-...",
180
+ "embedModel": "nomic-embed-text"
104
181
  }
105
182
  ```
106
183
 
107
- ## ๐Ÿ›  Build from Source
184
+ ---
185
+
186
+ ## Build from Source
108
187
 
109
188
  ```bash
110
189
  git clone https://github.com/maruakshay/miii-cli
111
190
  cd miii-cli && npm install && npm run build && npm link
112
191
  ```
113
192
 
114
- ## ๐ŸŒŸ Community & Philosophy
193
+ ---
194
+
195
+ ## The Bottom Line
196
+
197
+ The AI coding tools you're paying for right now will raise their prices, change their terms, and keep reading your code. **Miii won't.** It's MIT licensed, runs locally, and gets better every time Ollama ships a new model.
198
+
199
+ One engineer built a 176 KB tool that replaces a $200/month cloud product. That shouldn't be a surprise โ€” it should be the baseline.
200
+
201
+ If this saves you time or money, **star the repo**. It's the only metric that tells other engineers this is worth their attention.
202
+
203
+ **[โญ Star on GitHub](https://github.com/maruakshay/miii-cli)**
204
+
205
+ > Built by [@maruakshay](https://github.com/maruakshay) โ€” open to PRs, issues, and model recommendations.
115
206
 
116
- **Stop renting your intelligence. Own your AI stack.**
207
+ ---
117
208
 
118
- Miii is built for engineers who value privacy, speed, and total control. If this tool has accelerated your workflow, support the project:
119
- - ๐ŸŒŸ **Star the repo** on GitHub
120
- - ๐Ÿฆ **Share your wins** on X
121
- - ๐Ÿค– **Discuss the future** on Reddit
209
+ ## License
122
210
 
123
- ## ๐Ÿ“œ License
124
- MIT
211
+ MIT โ€” do whatever you want with it.
package/dist/init.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  import { loadConfig } from './config.js';
10
10
  import { SkillLoader } from './skills/loader.js';
11
11
  import { InputBar } from './tui/InputBar.js';
12
- import { welcome, setInkInstance } from './tui/printer.js';
12
+ import { welcome } from './tui/printer.js';
13
13
  import { ensureOllama } from './llm/ollama.js';
14
14
  const require = createRequire(import.meta.url);
15
15
  const UPDATE_CACHE = join(homedir(), '.config', 'miii', 'update-check.json');
@@ -90,7 +90,6 @@ export async function lazyInit() {
90
90
  // Print welcome banner to scrollback BEFORE Ink starts
91
91
  welcome(config.provider, config.model, process.cwd(), currentVersion, updateAvailable, linked);
92
92
  const sessionName = argv.session || `s-${Date.now()}`;
93
- const { waitUntilExit, clear } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
94
- setInkInstance(clear);
93
+ const { waitUntilExit } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
95
94
  await waitUntilExit();
96
95
  }
package/dist/sessions.js CHANGED
@@ -44,10 +44,29 @@ export function loadSession(name) {
44
44
  }
45
45
  export function saveSession(name, messages) {
46
46
  ensureDir();
47
- writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages));
47
+ try {
48
+ writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages));
49
+ }
50
+ catch { }
48
51
  }
49
52
  export function deleteSession(name) {
50
53
  const p = join(SESSIONS_DIR, `${sanitizeName(name)}.json`);
51
54
  if (existsSync(p))
52
55
  unlinkSync(p);
53
56
  }
57
+ export function deleteAllSessions(exceptName) {
58
+ ensureDir();
59
+ const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
60
+ let count = 0;
61
+ for (const f of files) {
62
+ const name = f.replace('.json', '');
63
+ if (exceptName && name === exceptName)
64
+ continue;
65
+ try {
66
+ unlinkSync(join(SESSIONS_DIR, f));
67
+ count++;
68
+ }
69
+ catch { }
70
+ }
71
+ return count;
72
+ }
@@ -12,6 +12,7 @@ export class MicroQueue {
12
12
  if (!this.heap.length)
13
13
  return undefined;
14
14
  const top = this.heap[0];
15
+ this.order.delete(top.id);
15
16
  const last = this.heap.pop();
16
17
  if (this.heap.length) {
17
18
  this.heap[0] = last;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { InputArea } from './components/InputArea.js';
5
5
  import { ModelPicker } from './components/ModelPicker.js';
@@ -9,7 +9,7 @@ import { readFile } from '../files/ops.js';
9
9
  import { generateId } from '../types.js';
10
10
  import * as printer from './printer.js';
11
11
  import { toolArgSummary } from './printer.js';
12
- import { loadSession, saveSession, listSessions, deleteSession } from '../sessions.js';
12
+ import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../sessions.js';
13
13
  import { MacroQueue, MicroQueue } from '../tasks/queue.js';
14
14
  import { TaskExecutor } from '../tasks/executor.js';
15
15
  import { fileEditContext } from '../tasks/compactor.js';
@@ -25,6 +25,7 @@ import { useSession } from './hooks/useSession.js';
25
25
  import { useModelPicker } from './hooks/useModelPicker.js';
26
26
  import { useRunLoop } from './hooks/useRunLoop.js';
27
27
  import { runDeepThink } from './deepThink.js';
28
+ import { setInkInstance } from './printer.js';
28
29
  const gitRun = promisify(exec);
29
30
  function buildAtContext(text) {
30
31
  const refs = [...text.matchAll(/@([\w./\-]+)/g)];
@@ -41,9 +42,20 @@ function buildAtContext(text) {
41
42
  }
42
43
  return parts.length ? parts.join('\n\n') + '\n\n' : '';
43
44
  }
45
+ function formatElapsed(ms) {
46
+ const s = Math.floor(ms / 1000);
47
+ if (s < 60)
48
+ return `${s}s`;
49
+ const m = Math.floor(s / 60);
50
+ const rem = s % 60;
51
+ return rem === 0 ? `${m}m` : `${m}m ${rem}s`;
52
+ }
44
53
  export function InputBar({ config, skills, cwd, session, version }) {
45
- const { stdout } = useStdout();
54
+ const { stdout, write: stdoutWrite } = useStdout();
46
55
  const cols = stdout.columns ?? 80;
56
+ useEffect(() => {
57
+ setInkInstance(stdoutWrite);
58
+ }, []);
47
59
  const phraseSeq = useMemo(() => Array.from({ length: 100 }, () => Math.floor(Math.random() * THINKING_PHRASES.length)), []);
48
60
  const [planningMode, setPlanningMode] = useState(false);
49
61
  const macroQueueRef = useRef(new MacroQueue());
@@ -445,7 +457,12 @@ export function InputBar({ config, skills, cwd, session, version }) {
445
457
  if (arg.startsWith('delete ')) {
446
458
  const target = arg.slice(7).trim();
447
459
  if (!target) {
448
- printer.systemMsg('usage: /session delete <name>');
460
+ printer.systemMsg('usage: /session delete <name|all>');
461
+ return;
462
+ }
463
+ if (target === 'all') {
464
+ const count = deleteAllSessions(sessionNameRef.current);
465
+ printer.systemMsg(`deleted ${count} session(s) โ€” kept active: ${sessionNameRef.current}`);
449
466
  return;
450
467
  }
451
468
  if (target === sessionNameRef.current) {
@@ -515,7 +532,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
515
532
  }, [skills, runLoop, openPicker]);
516
533
  const skillList = skills.list();
517
534
  // โ”€โ”€โ”€ render โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
518
- return (_jsxs(Box, { flexDirection: "column", children: [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 })] })) : permissionRequest ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, 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(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
519
- ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] })] })
520
- : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] }) }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, onSubmit: handleSubmit, onAbort: handleAbort })] }));
535
+ return (_jsxs(Box, { flexDirection: "column", children: [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 })] })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, 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) })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { children: status === 'thinking'
536
+ ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] })] })
537
+ : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_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, onSubmit: handleSubmit, onAbort: handleAbort, history: historyRef.current.filter(m => m.role === 'user').map(m => m.content) })] }));
521
538
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useMemo, useRef } from 'react';
3
- import { Box, Text, useInput } from 'ink';
3
+ import { Box, Text, useInput, useStdout } from 'ink';
4
4
  import { listFiles } from '../../files/ops.js';
5
5
  import { CommandPalette } from './CommandPalette.js';
6
6
  import { AtPicker } from './AtPicker.js';
@@ -32,18 +32,34 @@ const PLANNING_COMMANDS = [
32
32
  { ns: 'plan', name: 'review', description: 'review and critique the plan so far' },
33
33
  { ns: 'plan', name: 'done', description: 'exit planning mode' },
34
34
  ];
35
- const PASTE_MIN_LINES = 3;
36
- const PASTE_MIN_CHARS = 200;
37
- export function InputArea({ status, skills, cwd, planningMode, permissionRequest, onPermissionResponse, onSubmit, onAbort }) {
35
+ const PASTE_MIN_CHARS = 120;
36
+ function wordStartBefore(line, col) {
37
+ let i = col;
38
+ while (i > 0 && line[i - 1] === ' ')
39
+ i--;
40
+ while (i > 0 && line[i - 1] !== ' ')
41
+ i--;
42
+ return i;
43
+ }
44
+ function wordEndAfter(line, col) {
45
+ let i = col;
46
+ while (i < line.length && line[i] === ' ')
47
+ i++;
48
+ while (i < line.length && line[i] !== ' ')
49
+ i++;
50
+ return i;
51
+ }
52
+ export function InputArea({ status, skills, cwd, planningMode, permissionRequest, onPermissionResponse, onSubmit, onAbort, history = [] }) {
38
53
  const [lines, setLines] = useState(['']);
39
54
  const [cursor, setCursor] = useState({ row: 0, col: 0 });
40
55
  const [overlay, setOverlay] = useState('none');
41
56
  const [overlayIdx, setOverlayIdx] = useState(0);
42
57
  const [pasteLines, setPasteLines] = useState(0);
43
58
  const pasteRef = useRef(null);
59
+ const [historyIdx, setHistoryIdx] = useState(-1);
60
+ const savedInputRef = useRef('');
44
61
  const [files, setFiles] = useState([]);
45
62
  const filesLoadedRef = useRef(false);
46
- // built-ins first, then loaded skills (deduplicated by name)
47
63
  const allCommands = useMemo(() => {
48
64
  const builtinNames = new Set(BUILTIN_COMMANDS.map(b => b.name));
49
65
  const userSkills = skills.filter(s => !builtinNames.has(s.name));
@@ -61,7 +77,7 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
61
77
  return '';
62
78
  const after = before.slice(atIdx + 1);
63
79
  if (after.includes(' '))
64
- return ''; // space breaks @ ref
80
+ return '';
65
81
  return after;
66
82
  }, [lines, cursor]);
67
83
  const filteredCommands = useMemo(() => {
@@ -80,7 +96,9 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
80
96
  setTimeout(() => { try {
81
97
  setFiles(listFiles(cwd, true));
82
98
  }
83
- catch { } }, 0);
99
+ catch {
100
+ filesLoadedRef.current = false;
101
+ } }, 0);
84
102
  return [];
85
103
  }
86
104
  return files.filter(f => f.rel.toLowerCase().includes(atQuery.toLowerCase())).slice(0, 8);
@@ -93,6 +111,8 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
93
111
  setOverlayIdx(0);
94
112
  pasteRef.current = null;
95
113
  setPasteLines(0);
114
+ setHistoryIdx(-1);
115
+ savedInputRef.current = '';
96
116
  }
97
117
  function appendChar(ch) {
98
118
  setLines(prev => {
@@ -103,23 +123,45 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
103
123
  });
104
124
  setCursor(c => ({ ...c, col: c.col + ch.length }));
105
125
  }
106
- function deleteChar() {
126
+ function insertNewline() {
107
127
  const { row, col } = cursor;
128
+ const before = lines[row].slice(0, col);
129
+ const after = lines[row].slice(col);
108
130
  setLines(prev => {
109
131
  const next = [...prev];
110
- if (col > 0) {
111
- next[row] = next[row].slice(0, col - 1) + next[row].slice(col);
112
- }
113
- else if (row > 0) {
114
- const prevLen = next[row - 1].length;
115
- next.splice(row - 1, 2, next[row - 1] + next[row]);
116
- setCursor({ row: row - 1, col: prevLen });
117
- return next;
118
- }
132
+ next.splice(row, 1, before, after);
119
133
  return next;
120
134
  });
121
- if (col > 0)
135
+ setCursor({ row: row + 1, col: 0 });
136
+ }
137
+ function deleteChar() {
138
+ const { row, col } = cursor;
139
+ if (col > 0) {
140
+ setLines(prev => {
141
+ const next = [...prev];
142
+ next[row] = next[row].slice(0, col - 1) + next[row].slice(col);
143
+ return next;
144
+ });
122
145
  setCursor(c => ({ ...c, col: c.col - 1 }));
146
+ }
147
+ else if (row > 0) {
148
+ const prevLen = lines[row - 1].length;
149
+ setLines(prev => {
150
+ const next = [...prev];
151
+ next.splice(row - 1, 2, next[row - 1] + next[row]);
152
+ return next;
153
+ });
154
+ setCursor({ row: row - 1, col: prevLen });
155
+ }
156
+ }
157
+ function recallHistory(idx) {
158
+ const entry = history[history.length - 1 - idx];
159
+ if (!entry)
160
+ return;
161
+ const recalled = entry.split('\n');
162
+ setLines(recalled);
163
+ setCursor({ row: 0, col: recalled[0].length });
164
+ setHistoryIdx(idx);
123
165
  }
124
166
  function selectCommand(skill) {
125
167
  const name = (skill.ns === 'default' || skill.ns === 'builtin')
@@ -148,7 +190,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
148
190
  setOverlayIdx(0);
149
191
  }
150
192
  useInput((input, key) => {
151
- // Permission prompt intercepts all input
152
193
  if (permissionRequest && onPermissionResponse) {
153
194
  if (input === 'y' || input === 'Y') {
154
195
  onPermissionResponse(true);
@@ -160,7 +201,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
160
201
  }
161
202
  return;
162
203
  }
163
- // ESC: close overlay, abort stream, or clear input
164
204
  if (key.escape) {
165
205
  if (overlay !== 'none') {
166
206
  setOverlay('none');
@@ -174,7 +214,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
174
214
  clearInput();
175
215
  return;
176
216
  }
177
- // Ctrl+C
178
217
  if (key.ctrl && input === 'c') {
179
218
  if (status !== 'idle') {
180
219
  onAbort();
@@ -199,7 +238,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
199
238
  if (key.return) {
200
239
  if (overlay === 'command') {
201
240
  if (commandQuery.includes(' ')) {
202
- // has args โ€” submit full text, don't pick from palette
203
241
  const text = fullInput.trim();
204
242
  if (text) {
205
243
  clearInput();
@@ -215,7 +253,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
215
253
  }
216
254
  return;
217
255
  }
218
- // backspace/typing falls through to normal handling below
219
256
  }
220
257
  if (key.return) {
221
258
  const typed = fullInput.trim();
@@ -229,6 +266,11 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
229
266
  }
230
267
  return;
231
268
  }
269
+ // Ctrl+J โ€” insert newline without submitting
270
+ if (key.ctrl && input === 'j') {
271
+ insertNewline();
272
+ return;
273
+ }
232
274
  if (key.backspace || key.delete) {
233
275
  if (pasteRef.current) {
234
276
  pasteRef.current = null;
@@ -236,7 +278,6 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
236
278
  return;
237
279
  }
238
280
  deleteChar();
239
- // Recompute overlay trigger for updated input
240
281
  const r = cursor.row;
241
282
  const col = cursor.col;
242
283
  const prospectiveLine = col > 0
@@ -249,18 +290,96 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
249
290
  setOverlay('none');
250
291
  if (overlay === 'at') {
251
292
  const before = prospectiveLine.slice(0, Math.max(0, col - 1));
252
- const atIdx = before.lastIndexOf('@');
253
- if (atIdx === -1)
293
+ if (before.lastIndexOf('@') === -1)
254
294
  setOverlay('none');
255
295
  }
256
296
  return;
257
297
  }
298
+ // Ctrl chords
299
+ if (key.ctrl) {
300
+ const { row, col } = cursor;
301
+ const line = lines[row] ?? '';
302
+ if (input === 'a') {
303
+ setCursor(c => ({ ...c, col: 0 }));
304
+ return;
305
+ }
306
+ if (input === 'e') {
307
+ setCursor(c => ({ ...c, col: line.length }));
308
+ return;
309
+ }
310
+ if (input === 'w') {
311
+ if (col === 0)
312
+ return;
313
+ const newCol = wordStartBefore(line, col);
314
+ setLines(prev => {
315
+ const next = [...prev];
316
+ next[row] = line.slice(0, newCol) + line.slice(col);
317
+ return next;
318
+ });
319
+ setCursor(c => ({ ...c, col: newCol }));
320
+ return;
321
+ }
322
+ if (input === 'k') {
323
+ setLines(prev => {
324
+ const next = [...prev];
325
+ next[row] = line.slice(0, col);
326
+ return next;
327
+ });
328
+ return;
329
+ }
330
+ if (input === 'u') {
331
+ setLines(prev => {
332
+ const next = [...prev];
333
+ next[row] = '';
334
+ return next;
335
+ });
336
+ setCursor(c => ({ ...c, col: 0 }));
337
+ return;
338
+ }
339
+ if (key.leftArrow) {
340
+ setCursor(c => ({ ...c, col: wordStartBefore(line, col) }));
341
+ return;
342
+ }
343
+ if (key.rightArrow) {
344
+ setCursor(c => ({ ...c, col: wordEndAfter(line, col) }));
345
+ return;
346
+ }
347
+ return;
348
+ }
349
+ // Arrow keys
258
350
  if (key.upArrow && overlay === 'none') {
259
- setCursor(c => ({ row: Math.max(0, c.row - 1), col: 0 }));
351
+ if (cursor.row > 0) {
352
+ setCursor(c => ({ row: c.row - 1, col: Math.min(c.col, lines[c.row - 1]?.length ?? 0) }));
353
+ return;
354
+ }
355
+ // history recall at top row
356
+ if (history.length > 0) {
357
+ const nextIdx = historyIdx + 1;
358
+ if (nextIdx < history.length) {
359
+ if (historyIdx === -1)
360
+ savedInputRef.current = fullInput;
361
+ recallHistory(nextIdx);
362
+ }
363
+ }
260
364
  return;
261
365
  }
262
366
  if (key.downArrow && overlay === 'none') {
263
- setCursor(c => ({ row: Math.min(lines.length - 1, c.row + 1), col: 0 }));
367
+ if (cursor.row < lines.length - 1) {
368
+ setCursor(c => ({ row: c.row + 1, col: Math.min(c.col, lines[c.row + 1]?.length ?? 0) }));
369
+ return;
370
+ }
371
+ // history forward at bottom row
372
+ if (historyIdx > 0) {
373
+ recallHistory(historyIdx - 1);
374
+ }
375
+ else if (historyIdx === 0) {
376
+ const saved = savedInputRef.current;
377
+ const restored = saved ? saved.split('\n') : [''];
378
+ setLines(restored);
379
+ setCursor({ row: 0, col: restored[0].length });
380
+ setHistoryIdx(-1);
381
+ savedInputRef.current = '';
382
+ }
264
383
  return;
265
384
  }
266
385
  if (key.leftArrow) {
@@ -271,15 +390,18 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
271
390
  setCursor(c => ({ ...c, col: Math.min(lines[c.row]?.length ?? 0, c.col + 1) }));
272
391
  return;
273
392
  }
274
- if (input && !key.ctrl && !key.meta) {
275
- // Detect paste: Ink delivers entire pasted chunk as one input string
276
- const lineCount = input.split('\n').length;
277
- if (input.length > 1 && (lineCount >= PASTE_MIN_LINES || input.length >= PASTE_MIN_CHARS)) {
393
+ if (input && !key.meta) {
394
+ // Detect paste
395
+ const hasNewline = input.includes('\n');
396
+ const lineCount = hasNewline ? input.split('\n').length : 1;
397
+ if (input.length > 1 && (hasNewline || input.length >= PASTE_MIN_CHARS)) {
278
398
  pasteRef.current = input;
279
399
  setPasteLines(lineCount);
280
400
  return;
281
401
  }
282
- // Compute prospective new input to decide overlay
402
+ // Exit history mode on any edit
403
+ if (historyIdx !== -1)
404
+ setHistoryIdx(-1);
283
405
  const r = cursor.row;
284
406
  const col = cursor.col;
285
407
  const prospectiveLine = lines[r].slice(0, col) + input + lines[r].slice(col);
@@ -287,11 +409,9 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
287
409
  prospectiveLines[r] = prospectiveLine;
288
410
  const prospective = prospectiveLines.join('\n');
289
411
  appendChar(input);
290
- // Open/update overlays
291
412
  if (prospective.startsWith('/')) {
292
- const q = prospective.slice(1);
293
- if (q.includes(' ')) {
294
- setOverlay('none'); // typing args โ€” close palette, let user type freely
413
+ if (prospective.slice(1).includes(' ')) {
414
+ setOverlay('none');
295
415
  }
296
416
  else {
297
417
  setOverlay('command');
@@ -307,24 +427,32 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
307
427
  }
308
428
  }
309
429
  });
430
+ const { stdout } = useStdout();
431
+ const cols = stdout.columns ?? 80;
310
432
  const isProcessing = status !== 'idle';
311
- const borderColor = permissionRequest ? 'yellow' : isProcessing ? 'yellow' : 'cyan';
433
+ const promptColor = permissionRequest ? 'yellow' : isProcessing ? 'yellow' : 'green';
434
+ const inHistory = historyIdx !== -1;
312
435
  const hint = permissionRequest
313
- ? 'y approve n deny'
436
+ ? 'y approve ยท n deny'
314
437
  : isProcessing
315
- ? 'esc to abort'
438
+ ? 'esc to interrupt'
316
439
  : pasteLines > 0
317
- ? 'backspace removes paste enter to send'
440
+ ? 'backspace removes paste ยท enter to send'
318
441
  : overlay === 'command' && !commandQuery.includes(' ')
319
- ? 'โ†‘โ†“ navigate enter select esc close'
442
+ ? 'โ†‘โ†“ navigate ยท enter select ยท esc close'
320
443
  : overlay === 'at'
321
- ? 'โ†‘โ†“ navigate enter select esc close'
322
- : planningMode
323
- ? '๐Ÿ“‹ planning mode / suggestions enter send /plan:done to exit'
324
- : '@ file / command enter send ctrl+c exit';
325
- 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 })), _jsxs(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: borderColor, bold: true, children: 'โฏ ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_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, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " lines"] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { color: isActive ? 'white' : 'gray', dimColor: isProcessing, children: isActive ? 'โ–ˆ' : 'processing...' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
326
- ? renderLineWithCursor(line, cursor.col, isActive)
327
- : line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: hint })] })] }));
444
+ ? 'โ†‘โ†“ navigate ยท enter select ยท esc close'
445
+ : inHistory
446
+ ? `history [${historyIdx + 1}/${history.length}] ยท โ†‘โ†“ navigate ยท enter to send ยท esc clear`
447
+ : planningMode
448
+ ? 'planning mode ยท / suggestions ยท enter send ยท /plan:done exit'
449
+ : 'enter send ยท @ file ยท / cmd ยท ctrl+j newline ยท โ†‘ history';
450
+ const pastePreview = pasteRef.current
451
+ ? pasteRef.current.split('\n')[0].slice(0, cols - 6)
452
+ : '';
453
+ 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: 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] ? (isActive ? (_jsxs(Text, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "How can I help you? " }), _jsx(Text, { children: "\u2588" })] })) : (_jsx(Text, { color: "gray", dimColor: true, children: " " }))) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
454
+ ? renderLineWithCursor(line, cursor.col, isActive)
455
+ : line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: 'โ”€ ' + hint + ' ' + 'โ”€'.repeat(Math.max(0, cols - hint.length - 3)) })] }));
328
456
  }
329
457
  function renderLineWithCursor(line, col, showCursor) {
330
458
  return line.slice(0, col) + (showCursor ? 'โ–ˆ' : '') + line.slice(col);
@@ -34,6 +34,7 @@ Guardrails:
34
34
  return;
35
35
  depth++;
36
36
  let fullText = '';
37
+ let chatError = null;
37
38
  await chat({
38
39
  provider: config.provider,
39
40
  model,
@@ -41,10 +42,13 @@ Guardrails:
41
42
  apiKey: config.apiKey,
42
43
  messages: msgs,
43
44
  signal,
45
+ onChunk() { },
44
46
  async onDone(text) { fullText = text; },
45
47
  onError(err) { if (err.name !== 'AbortError')
46
- throw err; },
48
+ chatError = err; },
47
49
  });
50
+ if (chatError)
51
+ throw chatError;
48
52
  if (!fullText)
49
53
  return;
50
54
  const pending = [];
@@ -24,7 +24,8 @@ export async function buildGitContext(cwd, lastStatusRef) {
24
24
  if (code.includes('D'))
25
25
  continue;
26
26
  const raw = line.slice(3).trim().replace(/^"|"$/g, '');
27
- const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
27
+ const arrowIdx = raw.lastIndexOf(' -> ');
28
+ const rel = arrowIdx !== -1 ? raw.slice(arrowIdx + 4) : raw;
28
29
  if (!rel)
29
30
  continue;
30
31
  try {
@@ -33,6 +33,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
33
33
  }, [status]);
34
34
  const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
35
35
  if (depth >= MAX_TOOL_DEPTH) {
36
+ abortRef.current = null;
36
37
  setStatus('idle');
37
38
  return;
38
39
  }
@@ -1,11 +1,15 @@
1
1
  // ANSI-formatted stdout output โ€” goes into terminal scrollback
2
- let _inkClear = null;
3
- export function setInkInstance(clear) {
4
- _inkClear = clear;
2
+ let _inkWrite = null;
3
+ export function setInkInstance(inkWrite) {
4
+ _inkWrite = inkWrite;
5
5
  }
6
6
  function write(s) {
7
- _inkClear?.();
8
- process.stdout.write(s);
7
+ if (_inkWrite) {
8
+ _inkWrite(s);
9
+ }
10
+ else {
11
+ process.stdout.write(s);
12
+ }
9
13
  }
10
14
  const R = '\x1b[0m';
11
15
  const BOLD = '\x1b[1m';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.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",