miii-cli 0.2.1 → 0.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.
- package/README.md +190 -83
- package/dist/config.js +0 -1
- package/dist/files/ops.js +22 -4
- package/dist/index.js +0 -1
- package/dist/init.js +0 -1
- package/dist/llm/ollama.js +0 -1
- package/dist/llm/stream.js +4 -3
- package/dist/parser/stream-parser.js +1 -13
- package/dist/sessions.js +0 -1
- package/dist/skills/loader.js +0 -1
- package/dist/tasks/compactor.js +68 -0
- package/dist/tasks/executor.js +88 -0
- package/dist/tasks/queue.js +72 -0
- package/dist/tools/index.js +108 -5
- package/dist/tui/App.js +0 -1
- package/dist/tui/InputBar.js +379 -32
- package/dist/tui/components/AtPicker.js +0 -1
- package/dist/tui/components/CommandPalette.js +4 -3
- package/dist/tui/components/InputArea.js +25 -13
- package/dist/tui/components/MessageList.js +12 -1
- package/dist/tui/components/ModelPicker.js +0 -1
- package/dist/tui/components/StatusBar.js +0 -1
- package/dist/tui/printer.js +0 -1
- package/dist/types.js +0 -1
- package/dist/workers/context.worker.js +0 -1
- package/dist/workers/spawn.js +0 -1
- package/package.json +6 -3
- package/.claude/settings.local.json +0 -28
- package/CONTRIBUTING.md +0 -55
- package/Makefile +0 -13
- package/dist/config.d.ts +0 -2
- package/dist/config.js.map +0 -1
- package/dist/files/ops.d.ts +0 -14
- package/dist/files/ops.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/init.d.ts +0 -1
- package/dist/init.js.map +0 -1
- package/dist/llm/ollama.d.ts +0 -10
- package/dist/llm/ollama.js.map +0 -1
- package/dist/llm/stream.d.ts +0 -12
- package/dist/llm/stream.js.map +0 -1
- package/dist/parser/stream-parser.d.ts +0 -21
- package/dist/parser/stream-parser.js.map +0 -1
- package/dist/sessions.d.ts +0 -9
- package/dist/sessions.js.map +0 -1
- package/dist/skills/loader.d.ts +0 -23
- package/dist/skills/loader.js.map +0 -1
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js.map +0 -1
- package/dist/tui/App.d.ts +0 -9
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/InputBar.d.ts +0 -10
- package/dist/tui/InputBar.js.map +0 -1
- package/dist/tui/components/AtPicker.d.ts +0 -8
- package/dist/tui/components/AtPicker.js.map +0 -1
- package/dist/tui/components/CommandPalette.d.ts +0 -8
- package/dist/tui/components/CommandPalette.js.map +0 -1
- package/dist/tui/components/InputArea.d.ts +0 -12
- package/dist/tui/components/InputArea.js.map +0 -1
- package/dist/tui/components/MessageList.d.ts +0 -11
- package/dist/tui/components/MessageList.js.map +0 -1
- package/dist/tui/components/ModelPicker.d.ts +0 -18
- package/dist/tui/components/ModelPicker.js.map +0 -1
- package/dist/tui/components/StatusBar.d.ts +0 -12
- package/dist/tui/components/StatusBar.js.map +0 -1
- package/dist/tui/printer.d.ts +0 -7
- package/dist/tui/printer.js.map +0 -1
- package/dist/types.d.ts +0 -20
- package/dist/types.js.map +0 -1
- package/dist/workers/context.worker.js.map +0 -1
- package/dist/workers/diff.worker.d.ts +0 -1
- package/dist/workers/diff.worker.js +0 -12
- package/dist/workers/diff.worker.js.map +0 -1
- package/dist/workers/spawn.d.ts +0 -1
- package/dist/workers/spawn.js.map +0 -1
- package/install.sh +0 -6
- package/mii-cli.gif +0 -0
- package/src/config.ts +0 -32
- package/src/files/ops.ts +0 -89
- package/src/index.ts +0 -11
- package/src/init.ts +0 -41
- package/src/llm/ollama.ts +0 -110
- package/src/llm/stream.ts +0 -55
- package/src/parser/stream-parser.ts +0 -196
- package/src/sessions.ts +0 -54
- package/src/skills/loader.ts +0 -144
- package/src/tools/index.ts +0 -151
- package/src/tui/App.tsx +0 -355
- package/src/tui/InputBar.tsx +0 -381
- package/src/tui/components/AtPicker.tsx +0 -49
- package/src/tui/components/CommandPalette.tsx +0 -50
- package/src/tui/components/InputArea.tsx +0 -297
- package/src/tui/components/MessageList.tsx +0 -219
- package/src/tui/components/ModelPicker.tsx +0 -134
- package/src/tui/components/StatusBar.tsx +0 -36
- package/src/tui/printer.ts +0 -130
- package/src/types.ts +0 -26
- package/src/workers/context.worker.ts +0 -66
- package/src/workers/diff.worker.ts +0 -20
- package/src/workers/spawn.ts +0 -19
- package/tsconfig.json +0 -18
- /package/dist/{workers/context.worker.d.ts → tasks/types.js} +0 -0
package/src/tui/InputBar.tsx
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
-
import { Box, Text, useStdout } from 'ink'
|
|
3
|
-
import { InputArea } from './components/InputArea.js'
|
|
4
|
-
import { ModelPicker } from './components/ModelPicker.js'
|
|
5
|
-
import { Divider } from './components/StatusBar.js'
|
|
6
|
-
import { chat } from '../llm/stream.js'
|
|
7
|
-
import { listModels, pullModel } from '../llm/ollama.js'
|
|
8
|
-
import type { OllamaModel } from '../llm/ollama.js'
|
|
9
|
-
import { StreamParser } from '../parser/stream-parser.js'
|
|
10
|
-
import { tools, getSystemPrompt } from '../tools/index.js'
|
|
11
|
-
import { readFile } from '../files/ops.js'
|
|
12
|
-
import type { SkillLoader } from '../skills/loader.js'
|
|
13
|
-
import type { Status, ChatMessage, Config } from '../types.js'
|
|
14
|
-
import * as printer from './printer.js'
|
|
15
|
-
import { loadSession, saveSession, listSessions } from '../sessions.js'
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
config: Config
|
|
19
|
-
skills: SkillLoader
|
|
20
|
-
cwd: string
|
|
21
|
-
session: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const MAX_TOOL_DEPTH = 6
|
|
25
|
-
|
|
26
|
-
const THINKING_PHRASES = [
|
|
27
|
-
'oh wow, a question. let me pretend to care…',
|
|
28
|
-
'consulting the void…',
|
|
29
|
-
'making something up, just a sec…',
|
|
30
|
-
'definitely not hallucinating right now…',
|
|
31
|
-
'running 47 mental tabs…',
|
|
32
|
-
'staring into the abyss (it blinked)…',
|
|
33
|
-
'calculating your fate, no pressure…',
|
|
34
|
-
'doing the thinking you pay me for…',
|
|
35
|
-
'processing your questionable life choices…',
|
|
36
|
-
'summoning coherent thoughts, rarely works…',
|
|
37
|
-
]
|
|
38
|
-
const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹']
|
|
39
|
-
|
|
40
|
-
function buildAtContext(text: string): string {
|
|
41
|
-
const refs = [...text.matchAll(/@([\w./\-]+)/g)]
|
|
42
|
-
if (!refs.length) return ''
|
|
43
|
-
const parts: string[] = []
|
|
44
|
-
for (const m of refs) {
|
|
45
|
-
try {
|
|
46
|
-
const content = readFile(m[1])
|
|
47
|
-
if (content) parts.push(`<file path="${m[1]}">\n${content}\n</file>`)
|
|
48
|
-
} catch {}
|
|
49
|
-
}
|
|
50
|
-
return parts.length ? parts.join('\n\n') + '\n\n' : ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function InputBar({ config, skills, cwd, session }: Props) {
|
|
54
|
-
const { stdout } = useStdout()
|
|
55
|
-
const cols = stdout.columns ?? 80
|
|
56
|
-
|
|
57
|
-
const [status, setStatus] = useState<Status>('idle')
|
|
58
|
-
const [tick, setTick] = useState(0)
|
|
59
|
-
const [currentModel, setCurrentModel] = useState(config.model)
|
|
60
|
-
const [sessionName, setSessionName] = useState(session)
|
|
61
|
-
const [currentTool, setCurrentTool] = useState<string | undefined>()
|
|
62
|
-
const [planningMode, setPlanningMode] = useState(false)
|
|
63
|
-
|
|
64
|
-
// picker opens on mount — force model selection every launch
|
|
65
|
-
const [pickerOpen, setPickerOpen] = useState(true)
|
|
66
|
-
const [pickerModels, setPickerModels] = useState<OllamaModel[]>([])
|
|
67
|
-
const [pickerLoading, setPickerLoading] = useState(false)
|
|
68
|
-
const [pickerError, setPickerError] = useState<string | undefined>()
|
|
69
|
-
const [pullState, setPullState] = useState<{ name: string; status: string; pct: number | undefined } | undefined>()
|
|
70
|
-
|
|
71
|
-
const abortRef = useRef<AbortController | null>(null)
|
|
72
|
-
const pullAbortRef = useRef<AbortController | null>(null)
|
|
73
|
-
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`))
|
|
74
|
-
const currentModelRef = useRef(currentModel)
|
|
75
|
-
const sessionNameRef = useRef(sessionName)
|
|
76
|
-
const historyRef = useRef<ChatMessage[]>([])
|
|
77
|
-
|
|
78
|
-
useEffect(() => { currentModelRef.current = currentModel }, [currentModel])
|
|
79
|
-
useEffect(() => { sessionNameRef.current = sessionName }, [sessionName])
|
|
80
|
-
|
|
81
|
-
// mount: load session history + fetch models for initial picker
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
const history = loadSession(session)
|
|
84
|
-
historyRef.current = history
|
|
85
|
-
if (history.length) {
|
|
86
|
-
printer.systemMsg(`resumed "${session}" — ${history.length} messages`)
|
|
87
|
-
}
|
|
88
|
-
setPickerLoading(true)
|
|
89
|
-
listModels(config.baseUrl)
|
|
90
|
-
.then(m => { setPickerModels(m); setPickerLoading(false) })
|
|
91
|
-
.catch(e => { setPickerError(String(e)); setPickerLoading(false) })
|
|
92
|
-
}, [])
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (status === 'idle') return
|
|
96
|
-
const t = setInterval(() => setTick(n => n + 1), 80)
|
|
97
|
-
return () => clearInterval(t)
|
|
98
|
-
}, [status])
|
|
99
|
-
|
|
100
|
-
function buildContext(extra?: ChatMessage): ChatMessage[] {
|
|
101
|
-
const ctx: ChatMessage[] = [{ role: 'system', content: systemPromptRef.current }]
|
|
102
|
-
ctx.push(...historyRef.current)
|
|
103
|
-
if (extra) ctx.push(extra)
|
|
104
|
-
return ctx
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const runLoop = useCallback(async (contextMsgs: ChatMessage[], depth = 0) => {
|
|
108
|
-
if (depth >= MAX_TOOL_DEPTH) { setStatus('idle'); return }
|
|
109
|
-
setStatus('thinking')
|
|
110
|
-
|
|
111
|
-
abortRef.current = new AbortController()
|
|
112
|
-
|
|
113
|
-
await chat({
|
|
114
|
-
provider: config.provider,
|
|
115
|
-
model: currentModelRef.current,
|
|
116
|
-
baseUrl: config.baseUrl,
|
|
117
|
-
messages: contextMsgs,
|
|
118
|
-
signal: abortRef.current.signal,
|
|
119
|
-
|
|
120
|
-
async onDone(fullText) {
|
|
121
|
-
const pendingTools: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
122
|
-
const parser = new StreamParser()
|
|
123
|
-
for (const item of [...parser.feed(fullText), ...parser.flush()]) {
|
|
124
|
-
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
printer.assistantMsg(fullText)
|
|
128
|
-
historyRef.current.push({ role: 'assistant', content: fullText })
|
|
129
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
130
|
-
|
|
131
|
-
if (!pendingTools.length) { setStatus('idle'); return }
|
|
132
|
-
|
|
133
|
-
setStatus('tool')
|
|
134
|
-
const next: ChatMessage[] = [...contextMsgs, { role: 'assistant', content: fullText }]
|
|
135
|
-
|
|
136
|
-
for (const tc of pendingTools) {
|
|
137
|
-
const tool = tools.find(t => t.name === tc.name)
|
|
138
|
-
setCurrentTool(tc.name)
|
|
139
|
-
if (tool) {
|
|
140
|
-
try {
|
|
141
|
-
const result = await tool.execute(tc.args)
|
|
142
|
-
printer.toolMsg(tc.name, result)
|
|
143
|
-
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` })
|
|
144
|
-
} catch (e) {
|
|
145
|
-
const err = `Tool ${tc.name} error: ${e}`
|
|
146
|
-
printer.errorMsg(err)
|
|
147
|
-
next.push({ role: 'user', content: err })
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
printer.errorMsg(`unknown tool: ${tc.name}`)
|
|
151
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` })
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
setCurrentTool(undefined)
|
|
155
|
-
|
|
156
|
-
await runLoop(next, depth + 1)
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
onError(err) {
|
|
160
|
-
if (err.name !== 'AbortError') printer.errorMsg(err.message)
|
|
161
|
-
setStatus('idle')
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
}, [config])
|
|
165
|
-
|
|
166
|
-
// ─── model picker ──────────────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
const openPicker = useCallback(async () => {
|
|
169
|
-
setPickerOpen(true)
|
|
170
|
-
setPickerLoading(true)
|
|
171
|
-
setPickerError(undefined)
|
|
172
|
-
try { setPickerModels(await listModels(config.baseUrl)) }
|
|
173
|
-
catch (e) { setPickerError(String(e)) }
|
|
174
|
-
finally { setPickerLoading(false) }
|
|
175
|
-
}, [config.baseUrl])
|
|
176
|
-
|
|
177
|
-
const handleModelSelect = useCallback((name: string) => {
|
|
178
|
-
setCurrentModel(name)
|
|
179
|
-
currentModelRef.current = name
|
|
180
|
-
setPickerOpen(false)
|
|
181
|
-
printer.systemMsg(`model → ${name}`)
|
|
182
|
-
}, [])
|
|
183
|
-
|
|
184
|
-
const handleModelPull = useCallback(async (name: string) => {
|
|
185
|
-
setPullState({ name, status: 'starting...', pct: undefined })
|
|
186
|
-
pullAbortRef.current = new AbortController()
|
|
187
|
-
try {
|
|
188
|
-
await pullModel(config.baseUrl, name, (s, p) => setPullState({ name, status: s, pct: p }), pullAbortRef.current.signal)
|
|
189
|
-
setPickerModels(await listModels(config.baseUrl))
|
|
190
|
-
setPullState(undefined)
|
|
191
|
-
setCurrentModel(name)
|
|
192
|
-
currentModelRef.current = name
|
|
193
|
-
setPickerOpen(false)
|
|
194
|
-
printer.systemMsg(`pulled ${name} → active`)
|
|
195
|
-
} catch (e) {
|
|
196
|
-
setPullState(undefined)
|
|
197
|
-
setPickerError(`pull failed: ${e}`)
|
|
198
|
-
}
|
|
199
|
-
}, [config.baseUrl])
|
|
200
|
-
|
|
201
|
-
// ─── submit ────────────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
const handleSubmit = useCallback(async (text: string) => {
|
|
204
|
-
const cmd = text.trim()
|
|
205
|
-
|
|
206
|
-
if (cmd === '/models') { await openPicker(); return }
|
|
207
|
-
|
|
208
|
-
if (cmd === '/new') {
|
|
209
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
210
|
-
const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')
|
|
211
|
-
historyRef.current = []
|
|
212
|
-
setSessionName(newName)
|
|
213
|
-
setPlanningMode(false)
|
|
214
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
215
|
-
printer.systemMsg(`new session → ${newName}`)
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (cmd === '/clear') {
|
|
220
|
-
historyRef.current = []
|
|
221
|
-
saveSession(sessionNameRef.current, [])
|
|
222
|
-
setPlanningMode(false)
|
|
223
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
224
|
-
printer.systemMsg('chat cleared')
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (cmd === '/exit') { process.exit(0) }
|
|
229
|
-
|
|
230
|
-
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
231
|
-
const topic = cmd.slice(5).trim()
|
|
232
|
-
setPlanningMode(true)
|
|
233
|
-
systemPromptRef.current = getSystemPrompt(
|
|
234
|
-
`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure.`
|
|
235
|
-
)
|
|
236
|
-
const msg = topic
|
|
237
|
-
? `I want to plan: ${topic}`
|
|
238
|
-
: 'I want to start planning. Help me think through my goals step by step.'
|
|
239
|
-
printer.userMsg(msg)
|
|
240
|
-
historyRef.current.push({ role: 'user', content: msg })
|
|
241
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
242
|
-
await runLoop(buildContext())
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (cmd === '/plan:done') {
|
|
247
|
-
setPlanningMode(false)
|
|
248
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
249
|
-
printer.systemMsg('planning mode off')
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (cmd.startsWith('/plan:')) {
|
|
254
|
-
const subCmd = cmd.slice(6)
|
|
255
|
-
const subPrompts: Record<string, string> = {
|
|
256
|
-
next: 'What are the next concrete steps I should take?',
|
|
257
|
-
breakdown: 'Can you break this down into specific subtasks?',
|
|
258
|
-
review: 'Please review and critique our plan so far. What are we missing?',
|
|
259
|
-
}
|
|
260
|
-
const msg = subPrompts[subCmd]
|
|
261
|
-
if (msg) {
|
|
262
|
-
printer.userMsg(msg)
|
|
263
|
-
historyRef.current.push({ role: 'user', content: msg })
|
|
264
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
265
|
-
await runLoop(buildContext())
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (cmd === '/sessions') {
|
|
271
|
-
const sessions = listSessions()
|
|
272
|
-
if (!sessions.length) { printer.systemMsg('no saved sessions'); return }
|
|
273
|
-
printer.systemMsg(sessions.map(s =>
|
|
274
|
-
`${s.name === sessionNameRef.current ? '▶ ' : ' '}${s.name} (${s.messageCount} msgs)`
|
|
275
|
-
).join('\n'))
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (cmd.startsWith('/session')) {
|
|
280
|
-
const arg = cmd.slice(8).trim()
|
|
281
|
-
if (!arg) {
|
|
282
|
-
printer.systemMsg(`current: ${sessionNameRef.current}`)
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
286
|
-
historyRef.current = loadSession(arg)
|
|
287
|
-
setSessionName(arg)
|
|
288
|
-
printer.systemMsg(`session → ${arg} (${historyRef.current.length} messages)`)
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (text.startsWith('/')) {
|
|
293
|
-
const [slashCmd, ...rest] = text.slice(1).split(' ')
|
|
294
|
-
const skill = skills.get(slashCmd)
|
|
295
|
-
if (skill) {
|
|
296
|
-
if (skill.name === 'list') {
|
|
297
|
-
printer.systemMsg(skills.list().map(s =>
|
|
298
|
-
`/${s.ns === 'default' ? '' : s.ns + ':'}${s.name} — ${s.description}`
|
|
299
|
-
).join('\n'))
|
|
300
|
-
return
|
|
301
|
-
}
|
|
302
|
-
if (skill.execute) {
|
|
303
|
-
const ctx = {
|
|
304
|
-
messages: historyRef.current.map(m => ({ role: m.role, content: m.content })),
|
|
305
|
-
appendMessage: (_role: string, content: string) => printer.systemMsg(content),
|
|
306
|
-
setSystemPrompt: (p: string) => { systemPromptRef.current = p },
|
|
307
|
-
getSystemPrompt: () => systemPromptRef.current,
|
|
308
|
-
}
|
|
309
|
-
const result = await skill.execute(rest.join(' '), ctx)
|
|
310
|
-
if (result) printer.systemMsg(result)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
if (skill.prompt) {
|
|
314
|
-
printer.userMsg(skill.prompt)
|
|
315
|
-
historyRef.current.push({ role: 'user', content: skill.prompt })
|
|
316
|
-
await runLoop(buildContext())
|
|
317
|
-
return
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
printer.systemMsg(`unknown command: /${slashCmd} — try /list`)
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const contextPrefix = buildAtContext(text)
|
|
325
|
-
printer.userMsg(text)
|
|
326
|
-
historyRef.current.push({ role: 'user', content: contextPrefix + text })
|
|
327
|
-
saveSession(sessionNameRef.current, historyRef.current)
|
|
328
|
-
await runLoop(buildContext())
|
|
329
|
-
}, [skills, runLoop, openPicker])
|
|
330
|
-
|
|
331
|
-
const handleAbort = useCallback(() => {
|
|
332
|
-
abortRef.current?.abort()
|
|
333
|
-
setStatus('idle')
|
|
334
|
-
}, [])
|
|
335
|
-
|
|
336
|
-
const skillList = skills.list()
|
|
337
|
-
|
|
338
|
-
// ─── render ────────────────────────────────────────────────────────────────
|
|
339
|
-
|
|
340
|
-
return (
|
|
341
|
-
<Box flexDirection="column">
|
|
342
|
-
{pickerOpen ? (
|
|
343
|
-
<>
|
|
344
|
-
<ModelPicker
|
|
345
|
-
models={pickerModels}
|
|
346
|
-
current={currentModel}
|
|
347
|
-
loading={pickerLoading}
|
|
348
|
-
error={pickerError}
|
|
349
|
-
pull={pullState}
|
|
350
|
-
onSelect={handleModelSelect}
|
|
351
|
-
onPull={handleModelPull}
|
|
352
|
-
onClose={() => { setPickerOpen(false); setPullState(undefined) }}
|
|
353
|
-
/>
|
|
354
|
-
<Divider cols={cols} />
|
|
355
|
-
</>
|
|
356
|
-
) : (status === 'thinking' || status === 'tool') ? (
|
|
357
|
-
<>
|
|
358
|
-
<Box flexDirection="column" paddingX={1}>
|
|
359
|
-
<Text bold color="green">miii</Text>
|
|
360
|
-
<Box paddingLeft={2}>
|
|
361
|
-
{status === 'thinking'
|
|
362
|
-
? <><Text color="yellow">{SPARKLE[tick % SPARKLE.length]} </Text><Text color="gray" dimColor italic>{THINKING_PHRASES[Math.floor(tick / 62) % THINKING_PHRASES.length]}</Text></>
|
|
363
|
-
: <Text color="yellow" dimColor>⚙ running {currentTool ?? 'tool'}…</Text>
|
|
364
|
-
}
|
|
365
|
-
</Box>
|
|
366
|
-
</Box>
|
|
367
|
-
<Divider cols={cols} />
|
|
368
|
-
</>
|
|
369
|
-
) : null}
|
|
370
|
-
|
|
371
|
-
<InputArea
|
|
372
|
-
status={status}
|
|
373
|
-
skills={skillList}
|
|
374
|
-
cwd={cwd}
|
|
375
|
-
planningMode={planningMode}
|
|
376
|
-
onSubmit={handleSubmit}
|
|
377
|
-
onAbort={handleAbort}
|
|
378
|
-
/>
|
|
379
|
-
</Box>
|
|
380
|
-
)
|
|
381
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
3
|
-
import type { FileEntry } from '../../files/ops.js'
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
files: FileEntry[]
|
|
7
|
-
query: string
|
|
8
|
-
idx: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function AtPicker({ files, query, idx }: Props) {
|
|
12
|
-
const filtered = useMemo(() => {
|
|
13
|
-
if (!query) return files.slice(0, 8)
|
|
14
|
-
return files.filter(f => f.rel.toLowerCase().includes(query.toLowerCase())).slice(0, 8)
|
|
15
|
-
}, [files, query])
|
|
16
|
-
|
|
17
|
-
if (!filtered.length) {
|
|
18
|
-
return (
|
|
19
|
-
<Box borderStyle="round" borderColor="gray" marginX={1} paddingX={1}>
|
|
20
|
-
<Text color="gray">no files match "{query}"</Text>
|
|
21
|
-
</Box>
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<Box flexDirection="column" borderStyle="round" borderColor="gray" marginX={1}>
|
|
27
|
-
{filtered.map((f, i) => {
|
|
28
|
-
const active = i === idx
|
|
29
|
-
const icon = f.type === 'dir' ? '/' : ' '
|
|
30
|
-
return (
|
|
31
|
-
<Box key={f.path} paddingX={1}>
|
|
32
|
-
<Text color={active ? 'cyan' : 'white'} bold={active}>
|
|
33
|
-
{active ? '▶' : ' '}
|
|
34
|
-
{icon}
|
|
35
|
-
</Text>
|
|
36
|
-
<Text color={active ? 'cyan' : f.type === 'dir' ? 'blue' : 'white'}>
|
|
37
|
-
{' '}{f.rel}
|
|
38
|
-
</Text>
|
|
39
|
-
{f.size !== undefined && (
|
|
40
|
-
<Text color="gray" dimColor>
|
|
41
|
-
{' '}{f.size > 1024 ? `${(f.size / 1024).toFixed(0)}k` : `${f.size}b`}
|
|
42
|
-
</Text>
|
|
43
|
-
)}
|
|
44
|
-
</Box>
|
|
45
|
-
)
|
|
46
|
-
})}
|
|
47
|
-
</Box>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
3
|
-
import type { Skill } from '../../skills/loader.js'
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
skills: Skill[]
|
|
7
|
-
query: string
|
|
8
|
-
idx: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function CommandPalette({ skills, query, idx }: Props) {
|
|
12
|
-
const filtered = useMemo(() => {
|
|
13
|
-
const q = query.toLowerCase()
|
|
14
|
-
if (!q) return skills.slice(0, 10)
|
|
15
|
-
return skills.filter(s =>
|
|
16
|
-
s.name.includes(q) ||
|
|
17
|
-
`${s.ns}:${s.name}`.includes(q) ||
|
|
18
|
-
s.description.toLowerCase().includes(q)
|
|
19
|
-
).slice(0, 10)
|
|
20
|
-
}, [skills, query])
|
|
21
|
-
|
|
22
|
-
if (!filtered.length) {
|
|
23
|
-
return (
|
|
24
|
-
<Box borderStyle="round" borderColor="gray" marginX={1} paddingX={1}>
|
|
25
|
-
<Text color="gray">no commands match</Text>
|
|
26
|
-
</Box>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Box flexDirection="column" borderStyle="round" borderColor="gray" marginX={1}>
|
|
32
|
-
{filtered.map((s, i) => {
|
|
33
|
-
const active = i === idx
|
|
34
|
-
const isBuiltin = s.ns === 'builtin'
|
|
35
|
-
const name = (s.ns === 'default' || s.ns === 'builtin')
|
|
36
|
-
? `/${s.name}`
|
|
37
|
-
: `/${s.ns}:${s.name}`
|
|
38
|
-
return (
|
|
39
|
-
<Box key={`${s.ns}:${s.name}`} paddingX={1}>
|
|
40
|
-
<Text color={active ? 'cyan' : isBuiltin ? 'white' : 'magenta'} bold={active}>
|
|
41
|
-
{active ? '▶ ' : ' '}
|
|
42
|
-
{name.padEnd(20)}
|
|
43
|
-
</Text>
|
|
44
|
-
<Text color="gray" dimColor>{s.description}</Text>
|
|
45
|
-
</Box>
|
|
46
|
-
)
|
|
47
|
-
})}
|
|
48
|
-
</Box>
|
|
49
|
-
)
|
|
50
|
-
}
|