miii-cli 0.3.0 β 0.3.2
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 +158 -69
- package/dist/init.js +2 -3
- package/dist/sessions.js +20 -1
- package/dist/tasks/queue.js +1 -0
- package/dist/tui/InputBar.js +26 -8
- package/dist/tui/components/InputArea.js +193 -51
- package/dist/tui/deepThink.js +5 -1
- package/dist/tui/git-context.js +2 -1
- package/dist/tui/hooks/useRunLoop.js +26 -0
- package/dist/tui/printer.js +10 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Miii β The High-Performance Local AI Coding Agent
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|

|
|
6
6
|
|
|
@@ -9,87 +9,166 @@
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
---
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|---|---|---|---|---|
|
|
16
|
-
| **Runs locally** | β
Ollama / any API | β Cloud only | β Cloud only | β
Local + cloud |
|
|
17
|
-
| **Code stays private** | β
Never leaves machine | β Sent to Anthropic | β Sent to OpenAI | β οΈ Depends on model |
|
|
18
|
-
| **Cost** | π Free (your compute) | π³ Pay per token | π³ Pay per token | π Free (local) |
|
|
19
|
-
| **Runtime** | β‘ TypeScript β instant start | π Node (fast) | π Node | π’ Python |
|
|
20
|
-
| **Deep Think mode** | β
Gather + synthesize | β | β | β |
|
|
21
|
-
| **Auto-test loop** | β
Jest / Vitest / Mocha | β οΈ Manual | β | β οΈ Manual |
|
|
22
|
-
| **Web search built-in** | β
Tavily | β | β | β |
|
|
23
|
-
| **Surgical patch edits** | β
`patch_file` | β
| β οΈ | β
|
|
|
24
|
-
| **Session memory** | β
Named, persistent | β
| β | β οΈ Basic |
|
|
25
|
-
| **Skill / plugin system** | β
npm + `.md` skills | β οΈ MCP only | β | β |
|
|
26
|
-
| **Open source** | β
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
|
-
|
|
29
|
-
|
|
30
|
-
## β‘οΈ Quick Start
|
|
31
|
-
|
|
32
|
-
Get up and running in 30 seconds:
|
|
16
|
+
Zero subscription. Zero cloud dependency. Zero Python overhead. **176 KB total.** Just raw engineering horsepower in your terminal.
|
|
33
17
|
|
|
34
18
|
```bash
|
|
35
|
-
|
|
36
|
-
npm install -g miii-cli
|
|
37
|
-
miii
|
|
19
|
+
npm install -g miii-cli && miii
|
|
38
20
|
```
|
|
39
21
|
|
|
40
|
-
|
|
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.
|
|
41
27
|
|
|
42
|
-
|
|
28
|
+
**Miii does everything Claude Code does. It's 176 KB. It's free. It runs on your laptop.**
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
- **Blazing Fast**: Built with TypeScript for near-instant startup. No heavy Python runtime overhead. Tiny footprint, massive power.
|
|
46
|
-
- **Fully Autonomous**: Miii doesn't just suggest code; it acts as a junior engineerβediting files, running your test suite, and iterating until the bugs are gone.
|
|
47
|
-
- **Deep Context Awareness**: Automatically analyzes git diffs and project architecture, eliminating the need for manual copy-pasting.
|
|
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.
|
|
48
31
|
|
|
49
|
-
|
|
32
|
+
Miii flips the model. Your compute. Your data. Your rules.
|
|
50
33
|
|
|
51
|
-
|
|
52
|
-
- **π Auto-Test Loop**: Miii runs your Jest/Vitest/Mocha tests after every edit. If it breaks, it fixes itself.
|
|
53
|
-
- **π Web Intelligence**: Integrated `web_search` and `web_extract` via Tavily for real-time documentation.
|
|
54
|
-
- **π§ Deep Think**: Two-phase research mode β gathers from files, git, and web first, then synthesizes a complete answer. Available as `/think <query>` or as a tool the LLM calls autonomously.
|
|
55
|
-
- **π Planning Mode**: Use `/plan` to architect a solution before a single line of code is written.
|
|
56
|
-
- **π Session Memory**: Every conversation is auto-named and persisted. Resume your work instantly with `miii --session feature-auth`.
|
|
57
|
-
- **π¦ Skill System**: Extend miii with npm skill plugins or custom `.md` files.
|
|
34
|
+
---
|
|
58
35
|
|
|
59
|
-
##
|
|
36
|
+
## What Miii Actually Does
|
|
60
37
|
|
|
61
|
-
|
|
38
|
+
This isn't a fancy autocomplete. Miii is a **full autonomous agent loop:**
|
|
62
39
|
|
|
63
|
-
1.
|
|
64
|
-
2.
|
|
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
|
|
65
45
|
|
|
66
|
-
|
|
46
|
+
No babysitting. No copy-pasting error messages. No broken half-edits.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What a Session Looks Like
|
|
67
51
|
|
|
68
52
|
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
68
|
```
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
No prompts asking which files to change. No copy-pasting error messages. Just: describe the goal, watch it work.
|
|
71
|
+
|
|
72
|
+
---
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
## Killer Features
|
|
77
75
|
|
|
78
|
-
|
|
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
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 1. Start Ollama and pull a model
|
|
119
|
+
ollama pull qwen2.5-coder:7b
|
|
120
|
+
|
|
121
|
+
# 2. Install Miii
|
|
122
|
+
npm install -g miii-cli
|
|
123
|
+
|
|
124
|
+
# 3. Go to your project and start
|
|
125
|
+
cd your-project
|
|
126
|
+
miii
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
That's it. No API keys. No account. No sign-up form.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Power Commands
|
|
79
134
|
|
|
80
135
|
| Command | What it does |
|
|
81
136
|
|---|---|
|
|
82
|
-
| `/think <
|
|
83
|
-
| `/refactor <goal>` |
|
|
84
|
-
| `/
|
|
85
|
-
| `/
|
|
86
|
-
| `/
|
|
87
|
-
| `/
|
|
88
|
-
| `/
|
|
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 |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Semantic Codebase Indexing
|
|
151
|
+
|
|
152
|
+
For large codebases, Miii can build and query a local vector index β no third-party APIs, no embeddings sent anywhere.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
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
|
|
163
|
+
```
|
|
89
164
|
|
|
90
|
-
|
|
165
|
+
The agent calls `search_codebase` on its own when needed. You don't have to think about it.
|
|
91
166
|
|
|
92
|
-
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
|
|
93
172
|
|
|
94
173
|
```json
|
|
95
174
|
{
|
|
@@ -97,26 +176,36 @@ Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
|
|
|
97
176
|
"provider": "ollama",
|
|
98
177
|
"baseUrl": "http://localhost:11434",
|
|
99
178
|
"gitContext": true,
|
|
100
|
-
"tavilyApiKey": "tvly-..."
|
|
179
|
+
"tavilyApiKey": "tvly-...",
|
|
180
|
+
"embedModel": "nomic-embed-text"
|
|
101
181
|
}
|
|
102
182
|
```
|
|
103
183
|
|
|
104
|
-
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Build from Source
|
|
105
187
|
|
|
106
188
|
```bash
|
|
107
189
|
git clone https://github.com/maruakshay/miii-cli
|
|
108
190
|
cd miii-cli && npm install && npm run build && npm link
|
|
109
191
|
```
|
|
110
192
|
|
|
111
|
-
|
|
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.
|
|
112
206
|
|
|
113
|
-
|
|
207
|
+
---
|
|
114
208
|
|
|
115
|
-
|
|
116
|
-
- π **Star the repo** on GitHub
|
|
117
|
-
- π¦ **Share on X**
|
|
118
|
-
- π€ **Post on Reddit**
|
|
119
|
-
- π¬ **Tell a fellow developer**
|
|
209
|
+
## License
|
|
120
210
|
|
|
121
|
-
|
|
122
|
-
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/tasks/queue.js
CHANGED
package/dist/tui/InputBar.js
CHANGED
|
@@ -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';
|
|
@@ -8,7 +8,8 @@ import { tools } from '../tools/index.js';
|
|
|
8
8
|
import { readFile } from '../files/ops.js';
|
|
9
9
|
import { generateId } from '../types.js';
|
|
10
10
|
import * as printer from './printer.js';
|
|
11
|
-
import {
|
|
11
|
+
import { toolArgSummary } from './printer.js';
|
|
12
|
+
import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../sessions.js';
|
|
12
13
|
import { MacroQueue, MicroQueue } from '../tasks/queue.js';
|
|
13
14
|
import { TaskExecutor } from '../tasks/executor.js';
|
|
14
15
|
import { fileEditContext } from '../tasks/compactor.js';
|
|
@@ -24,6 +25,7 @@ import { useSession } from './hooks/useSession.js';
|
|
|
24
25
|
import { useModelPicker } from './hooks/useModelPicker.js';
|
|
25
26
|
import { useRunLoop } from './hooks/useRunLoop.js';
|
|
26
27
|
import { runDeepThink } from './deepThink.js';
|
|
28
|
+
import { setInkInstance } from './printer.js';
|
|
27
29
|
const gitRun = promisify(exec);
|
|
28
30
|
function buildAtContext(text) {
|
|
29
31
|
const refs = [...text.matchAll(/@([\w./\-]+)/g)];
|
|
@@ -40,9 +42,20 @@ function buildAtContext(text) {
|
|
|
40
42
|
}
|
|
41
43
|
return parts.length ? parts.join('\n\n') + '\n\n' : '';
|
|
42
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
|
+
}
|
|
43
53
|
export function InputBar({ config, skills, cwd, session, version }) {
|
|
44
|
-
const { stdout } = useStdout();
|
|
54
|
+
const { stdout, write: stdoutWrite } = useStdout();
|
|
45
55
|
const cols = stdout.columns ?? 80;
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setInkInstance(stdoutWrite);
|
|
58
|
+
}, []);
|
|
46
59
|
const phraseSeq = useMemo(() => Array.from({ length: 100 }, () => Math.floor(Math.random() * THINKING_PHRASES.length)), []);
|
|
47
60
|
const [planningMode, setPlanningMode] = useState(false);
|
|
48
61
|
const macroQueueRef = useRef(new MacroQueue());
|
|
@@ -61,7 +74,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
|
|
|
61
74
|
},
|
|
62
75
|
}), [config]);
|
|
63
76
|
const allTools = useMemo(() => [...tools, deepThinkTool], [deepThinkTool]);
|
|
64
|
-
const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
|
|
77
|
+
const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
|
|
65
78
|
// βββ refactor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
66
79
|
const runRefactor = useCallback(async (goal) => {
|
|
67
80
|
printer.systemMsg(`refactor: ${goal}`);
|
|
@@ -444,7 +457,12 @@ export function InputBar({ config, skills, cwd, session, version }) {
|
|
|
444
457
|
if (arg.startsWith('delete ')) {
|
|
445
458
|
const target = arg.slice(7).trim();
|
|
446
459
|
if (!target) {
|
|
447
|
-
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}`);
|
|
448
466
|
return;
|
|
449
467
|
}
|
|
450
468
|
if (target === sessionNameRef.current) {
|
|
@@ -514,7 +532,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
|
|
|
514
532
|
}, [skills, runLoop, openPicker]);
|
|
515
533
|
const skillList = skills.list();
|
|
516
534
|
// βββ render ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
517
|
-
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 })] })) : (
|
|
518
|
-
|
|
519
|
-
|
|
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) })] }));
|
|
520
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
|
|
36
|
-
|
|
37
|
-
|
|
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, onSubmit, onAbort
|
|
|
61
77
|
return '';
|
|
62
78
|
const after = before.slice(atIdx + 1);
|
|
63
79
|
if (after.includes(' '))
|
|
64
|
-
return '';
|
|
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, onSubmit, onAbort
|
|
|
80
96
|
setTimeout(() => { try {
|
|
81
97
|
setFiles(listFiles(cwd, true));
|
|
82
98
|
}
|
|
83
|
-
catch {
|
|
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, onSubmit, onAbort
|
|
|
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, onSubmit, onAbort
|
|
|
103
123
|
});
|
|
104
124
|
setCursor(c => ({ ...c, col: c.col + ch.length }));
|
|
105
125
|
}
|
|
106
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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,17 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
148
190
|
setOverlayIdx(0);
|
|
149
191
|
}
|
|
150
192
|
useInput((input, key) => {
|
|
151
|
-
|
|
193
|
+
if (permissionRequest && onPermissionResponse) {
|
|
194
|
+
if (input === 'y' || input === 'Y') {
|
|
195
|
+
onPermissionResponse(true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
199
|
+
onPermissionResponse(false);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
152
204
|
if (key.escape) {
|
|
153
205
|
if (overlay !== 'none') {
|
|
154
206
|
setOverlay('none');
|
|
@@ -162,7 +214,6 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
162
214
|
clearInput();
|
|
163
215
|
return;
|
|
164
216
|
}
|
|
165
|
-
// Ctrl+C
|
|
166
217
|
if (key.ctrl && input === 'c') {
|
|
167
218
|
if (status !== 'idle') {
|
|
168
219
|
onAbort();
|
|
@@ -187,7 +238,6 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
187
238
|
if (key.return) {
|
|
188
239
|
if (overlay === 'command') {
|
|
189
240
|
if (commandQuery.includes(' ')) {
|
|
190
|
-
// has args β submit full text, don't pick from palette
|
|
191
241
|
const text = fullInput.trim();
|
|
192
242
|
if (text) {
|
|
193
243
|
clearInput();
|
|
@@ -203,7 +253,6 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
203
253
|
}
|
|
204
254
|
return;
|
|
205
255
|
}
|
|
206
|
-
// backspace/typing falls through to normal handling below
|
|
207
256
|
}
|
|
208
257
|
if (key.return) {
|
|
209
258
|
const typed = fullInput.trim();
|
|
@@ -217,6 +266,11 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
217
266
|
}
|
|
218
267
|
return;
|
|
219
268
|
}
|
|
269
|
+
// Ctrl+J β insert newline without submitting
|
|
270
|
+
if (key.ctrl && input === 'j') {
|
|
271
|
+
insertNewline();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
220
274
|
if (key.backspace || key.delete) {
|
|
221
275
|
if (pasteRef.current) {
|
|
222
276
|
pasteRef.current = null;
|
|
@@ -224,7 +278,6 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
224
278
|
return;
|
|
225
279
|
}
|
|
226
280
|
deleteChar();
|
|
227
|
-
// Recompute overlay trigger for updated input
|
|
228
281
|
const r = cursor.row;
|
|
229
282
|
const col = cursor.col;
|
|
230
283
|
const prospectiveLine = col > 0
|
|
@@ -237,18 +290,96 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
237
290
|
setOverlay('none');
|
|
238
291
|
if (overlay === 'at') {
|
|
239
292
|
const before = prospectiveLine.slice(0, Math.max(0, col - 1));
|
|
240
|
-
|
|
241
|
-
if (atIdx === -1)
|
|
293
|
+
if (before.lastIndexOf('@') === -1)
|
|
242
294
|
setOverlay('none');
|
|
243
295
|
}
|
|
244
296
|
return;
|
|
245
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
|
|
246
350
|
if (key.upArrow && overlay === 'none') {
|
|
247
|
-
|
|
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
|
+
}
|
|
248
364
|
return;
|
|
249
365
|
}
|
|
250
366
|
if (key.downArrow && overlay === 'none') {
|
|
251
|
-
|
|
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
|
+
}
|
|
252
383
|
return;
|
|
253
384
|
}
|
|
254
385
|
if (key.leftArrow) {
|
|
@@ -259,15 +390,18 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
259
390
|
setCursor(c => ({ ...c, col: Math.min(lines[c.row]?.length ?? 0, c.col + 1) }));
|
|
260
391
|
return;
|
|
261
392
|
}
|
|
262
|
-
if (input && !key.
|
|
263
|
-
// Detect paste
|
|
264
|
-
const
|
|
265
|
-
|
|
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)) {
|
|
266
398
|
pasteRef.current = input;
|
|
267
399
|
setPasteLines(lineCount);
|
|
268
400
|
return;
|
|
269
401
|
}
|
|
270
|
-
//
|
|
402
|
+
// Exit history mode on any edit
|
|
403
|
+
if (historyIdx !== -1)
|
|
404
|
+
setHistoryIdx(-1);
|
|
271
405
|
const r = cursor.row;
|
|
272
406
|
const col = cursor.col;
|
|
273
407
|
const prospectiveLine = lines[r].slice(0, col) + input + lines[r].slice(col);
|
|
@@ -275,11 +409,9 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
275
409
|
prospectiveLines[r] = prospectiveLine;
|
|
276
410
|
const prospective = prospectiveLines.join('\n');
|
|
277
411
|
appendChar(input);
|
|
278
|
-
// Open/update overlays
|
|
279
412
|
if (prospective.startsWith('/')) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
setOverlay('none'); // typing args β close palette, let user type freely
|
|
413
|
+
if (prospective.slice(1).includes(' ')) {
|
|
414
|
+
setOverlay('none');
|
|
283
415
|
}
|
|
284
416
|
else {
|
|
285
417
|
setOverlay('command');
|
|
@@ -295,22 +427,32 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
295
427
|
}
|
|
296
428
|
}
|
|
297
429
|
});
|
|
430
|
+
const { stdout } = useStdout();
|
|
431
|
+
const cols = stdout.columns ?? 80;
|
|
298
432
|
const isProcessing = status !== 'idle';
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
433
|
+
const promptColor = permissionRequest ? 'yellow' : isProcessing ? 'yellow' : 'green';
|
|
434
|
+
const inHistory = historyIdx !== -1;
|
|
435
|
+
const hint = permissionRequest
|
|
436
|
+
? 'y approve Β· n deny'
|
|
437
|
+
: isProcessing
|
|
438
|
+
? 'esc to interrupt'
|
|
439
|
+
: pasteLines > 0
|
|
440
|
+
? 'backspace removes paste Β· enter to send'
|
|
441
|
+
: overlay === 'command' && !commandQuery.includes(' ')
|
|
442
|
+
? 'ββ navigate Β· enter select Β· esc close'
|
|
443
|
+
: overlay === 'at'
|
|
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)) })] }));
|
|
314
456
|
}
|
|
315
457
|
function renderLineWithCursor(line, col, showCursor) {
|
|
316
458
|
return line.slice(0, col) + (showCursor ? 'β' : '') + line.slice(col);
|
package/dist/tui/deepThink.js
CHANGED
|
@@ -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
|
-
|
|
48
|
+
chatError = err; },
|
|
47
49
|
});
|
|
50
|
+
if (chatError)
|
|
51
|
+
throw chatError;
|
|
48
52
|
if (!fullText)
|
|
49
53
|
return;
|
|
50
54
|
const pending = [];
|
package/dist/tui/git-context.js
CHANGED
|
@@ -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
|
|
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 {
|
|
@@ -7,16 +7,24 @@ import * as printer from '../printer.js';
|
|
|
7
7
|
const MAX_TOOL_DEPTH = 6;
|
|
8
8
|
const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'patch_file', 'delete_file']);
|
|
9
9
|
const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
|
|
10
|
+
const PERMISSION_TOOLS = new Set(['edit_file', 'patch_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
|
|
10
11
|
export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef) {
|
|
11
12
|
const [status, setStatus] = useState('idle');
|
|
12
13
|
const [tick, setTick] = useState(0);
|
|
13
14
|
const [currentTool, setCurrentTool] = useState();
|
|
14
15
|
const [taskLabel, setTaskLabel] = useState();
|
|
16
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
17
|
+
const permissionResolveRef = useRef(null);
|
|
15
18
|
const thinkingStartRef = useRef(0);
|
|
16
19
|
const extraToolsRef = useRef(extraTools);
|
|
17
20
|
extraToolsRef.current = extraTools;
|
|
18
21
|
const pushHistoryRef = useRef(pushHistory);
|
|
19
22
|
useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
|
|
23
|
+
const resolvePermission = useCallback((approved) => {
|
|
24
|
+
permissionResolveRef.current?.(approved);
|
|
25
|
+
permissionResolveRef.current = null;
|
|
26
|
+
setPermissionRequest(null);
|
|
27
|
+
}, []);
|
|
20
28
|
useEffect(() => {
|
|
21
29
|
if (status === 'idle')
|
|
22
30
|
return;
|
|
@@ -25,6 +33,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
25
33
|
}, [status]);
|
|
26
34
|
const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
|
|
27
35
|
if (depth >= MAX_TOOL_DEPTH) {
|
|
36
|
+
abortRef.current = null;
|
|
28
37
|
setStatus('idle');
|
|
29
38
|
return;
|
|
30
39
|
}
|
|
@@ -78,6 +87,17 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
78
87
|
const allTools = [...staticTools, ...extraToolsRef.current];
|
|
79
88
|
const tool = allTools.find(t => t.name === tc.name);
|
|
80
89
|
setCurrentTool(tc.name);
|
|
90
|
+
if (PERMISSION_TOOLS.has(tc.name)) {
|
|
91
|
+
const approved = await new Promise(resolve => {
|
|
92
|
+
permissionResolveRef.current = resolve;
|
|
93
|
+
setPermissionRequest({ toolName: tc.name, args: tc.args });
|
|
94
|
+
});
|
|
95
|
+
if (!approved) {
|
|
96
|
+
printer.systemMsg(`denied: ${tc.name}`);
|
|
97
|
+
next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user` });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
81
101
|
if (tool) {
|
|
82
102
|
try {
|
|
83
103
|
printer.toolCallStart(tc.name, tc.args);
|
|
@@ -136,6 +156,11 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
136
156
|
}, [config]);
|
|
137
157
|
const handleAbort = useCallback(() => {
|
|
138
158
|
abortRef.current?.abort();
|
|
159
|
+
if (permissionResolveRef.current) {
|
|
160
|
+
permissionResolveRef.current(false);
|
|
161
|
+
permissionResolveRef.current = null;
|
|
162
|
+
setPermissionRequest(null);
|
|
163
|
+
}
|
|
139
164
|
setStatus('idle');
|
|
140
165
|
}, []);
|
|
141
166
|
return {
|
|
@@ -144,5 +169,6 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
144
169
|
taskLabel, setTaskLabel,
|
|
145
170
|
thinkingStartRef,
|
|
146
171
|
runLoop, handleAbort,
|
|
172
|
+
permissionRequest, resolvePermission,
|
|
147
173
|
};
|
|
148
174
|
}
|
package/dist/tui/printer.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// ANSI-formatted stdout output β goes into terminal scrollback
|
|
2
|
-
let
|
|
3
|
-
export function setInkInstance(
|
|
4
|
-
|
|
2
|
+
let _inkWrite = null;
|
|
3
|
+
export function setInkInstance(inkWrite) {
|
|
4
|
+
_inkWrite = inkWrite;
|
|
5
5
|
}
|
|
6
6
|
function write(s) {
|
|
7
|
-
|
|
8
|
-
|
|
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';
|
|
@@ -63,7 +67,7 @@ function formatContent(text) {
|
|
|
63
67
|
function truncate(s, n) {
|
|
64
68
|
return s.length > n ? s.slice(0, n) + 'β¦' : s;
|
|
65
69
|
}
|
|
66
|
-
function toolArgSummary(args) {
|
|
70
|
+
export function toolArgSummary(args) {
|
|
67
71
|
if (args.message)
|
|
68
72
|
return `"${truncate(String(args.message), 60)}"`;
|
|
69
73
|
if (args.path)
|