miii-cli 0.1.8 → 0.2.1
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/.claude/settings.local.json +8 -1
- package/README.md +43 -8
- package/dist/llm/stream.d.ts +2 -3
- package/dist/llm/stream.js +22 -100
- package/dist/llm/stream.js.map +1 -1
- package/dist/tui/App.js +10 -34
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/InputBar.js +23 -37
- package/dist/tui/InputBar.js.map +1 -1
- package/dist/tui/components/MessageList.d.ts +2 -1
- package/dist/tui/components/MessageList.js +23 -5
- package/dist/tui/components/MessageList.js.map +1 -1
- package/dist/tui/components/StatusBar.js +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/printer.js +1 -1
- package/dist/tui/printer.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/llm/stream.ts +18 -82
- package/src/tui/App.tsx +11 -34
- package/src/tui/InputBar.tsx +30 -42
- package/src/tui/components/MessageList.tsx +33 -6
- package/src/tui/components/StatusBar.tsx +1 -1
- package/src/tui/printer.ts +1 -1
- package/src/types.ts +1 -1
package/src/tui/InputBar.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Box, Text, useStdout } from 'ink'
|
|
|
3
3
|
import { InputArea } from './components/InputArea.js'
|
|
4
4
|
import { ModelPicker } from './components/ModelPicker.js'
|
|
5
5
|
import { Divider } from './components/StatusBar.js'
|
|
6
|
-
import {
|
|
6
|
+
import { chat } from '../llm/stream.js'
|
|
7
7
|
import { listModels, pullModel } from '../llm/ollama.js'
|
|
8
8
|
import type { OllamaModel } from '../llm/ollama.js'
|
|
9
9
|
import { StreamParser } from '../parser/stream-parser.js'
|
|
@@ -22,12 +22,20 @@ interface Props {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const MAX_TOOL_DEPTH = 6
|
|
25
|
-
const THROTTLE_MS = 40
|
|
26
|
-
const PREVIEW_LINES = 8
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 = ['✦', '✧', '✶', '✷', '✸', '✹']
|
|
31
39
|
|
|
32
40
|
function buildAtContext(text: string): string {
|
|
33
41
|
const refs = [...text.matchAll(/@([\w./\-]+)/g)]
|
|
@@ -49,8 +57,8 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
49
57
|
const [status, setStatus] = useState<Status>('idle')
|
|
50
58
|
const [tick, setTick] = useState(0)
|
|
51
59
|
const [currentModel, setCurrentModel] = useState(config.model)
|
|
52
|
-
const [streamPreview, setStreamPreview] = useState('')
|
|
53
60
|
const [sessionName, setSessionName] = useState(session)
|
|
61
|
+
const [currentTool, setCurrentTool] = useState<string | undefined>()
|
|
54
62
|
const [planningMode, setPlanningMode] = useState(false)
|
|
55
63
|
|
|
56
64
|
// picker opens on mount — force model selection every launch
|
|
@@ -62,8 +70,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
62
70
|
|
|
63
71
|
const abortRef = useRef<AbortController | null>(null)
|
|
64
72
|
const pullAbortRef = useRef<AbortController | null>(null)
|
|
65
|
-
const tokenBufRef = useRef('')
|
|
66
|
-
const lastRenderRef = useRef(0)
|
|
67
73
|
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`))
|
|
68
74
|
const currentModelRef = useRef(currentModel)
|
|
69
75
|
const sessionNameRef = useRef(sessionName)
|
|
@@ -100,45 +106,25 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
100
106
|
|
|
101
107
|
const runLoop = useCallback(async (contextMsgs: ChatMessage[], depth = 0) => {
|
|
102
108
|
if (depth >= MAX_TOOL_DEPTH) { setStatus('idle'); return }
|
|
103
|
-
setStatus('
|
|
104
|
-
setStreamPreview('')
|
|
105
|
-
|
|
106
|
-
const parser = new StreamParser()
|
|
107
|
-
const pendingTools: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
108
|
-
let fullText = ''
|
|
109
|
+
setStatus('thinking')
|
|
109
110
|
|
|
110
111
|
abortRef.current = new AbortController()
|
|
111
112
|
|
|
112
|
-
await
|
|
113
|
+
await chat({
|
|
113
114
|
provider: config.provider,
|
|
114
115
|
model: currentModelRef.current,
|
|
115
116
|
baseUrl: config.baseUrl,
|
|
116
117
|
messages: contextMsgs,
|
|
117
118
|
signal: abortRef.current.signal,
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if (now - lastRenderRef.current >= THROTTLE_MS) {
|
|
124
|
-
setStreamPreview(fullText)
|
|
125
|
-
tokenBufRef.current = ''
|
|
126
|
-
lastRenderRef.current = now
|
|
127
|
-
}
|
|
128
|
-
for (const item of parser.feed(token)) {
|
|
129
|
-
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
async onDone() {
|
|
134
|
-
setStreamPreview(fullText)
|
|
135
|
-
for (const item of parser.flush()) {
|
|
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()]) {
|
|
136
124
|
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
137
125
|
}
|
|
138
126
|
|
|
139
127
|
printer.assistantMsg(fullText)
|
|
140
|
-
setStreamPreview('')
|
|
141
|
-
|
|
142
128
|
historyRef.current.push({ role: 'assistant', content: fullText })
|
|
143
129
|
saveSession(sessionNameRef.current, historyRef.current)
|
|
144
130
|
|
|
@@ -149,6 +135,7 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
149
135
|
|
|
150
136
|
for (const tc of pendingTools) {
|
|
151
137
|
const tool = tools.find(t => t.name === tc.name)
|
|
138
|
+
setCurrentTool(tc.name)
|
|
152
139
|
if (tool) {
|
|
153
140
|
try {
|
|
154
141
|
const result = await tool.execute(tc.args)
|
|
@@ -164,6 +151,7 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
164
151
|
next.push({ role: 'user', content: `unknown tool: ${tc.name}` })
|
|
165
152
|
}
|
|
166
153
|
}
|
|
154
|
+
setCurrentTool(undefined)
|
|
167
155
|
|
|
168
156
|
await runLoop(next, depth + 1)
|
|
169
157
|
},
|
|
@@ -171,7 +159,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
171
159
|
onError(err) {
|
|
172
160
|
if (err.name !== 'AbortError') printer.errorMsg(err.message)
|
|
173
161
|
setStatus('idle')
|
|
174
|
-
setStreamPreview('')
|
|
175
162
|
},
|
|
176
163
|
})
|
|
177
164
|
}, [config])
|
|
@@ -344,8 +331,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
344
331
|
const handleAbort = useCallback(() => {
|
|
345
332
|
abortRef.current?.abort()
|
|
346
333
|
setStatus('idle')
|
|
347
|
-
setStreamPreview('')
|
|
348
|
-
tokenBufRef.current = ''
|
|
349
334
|
}, [])
|
|
350
335
|
|
|
351
336
|
const skillList = skills.list()
|
|
@@ -368,13 +353,16 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
368
353
|
/>
|
|
369
354
|
<Divider cols={cols} />
|
|
370
355
|
</>
|
|
371
|
-
) :
|
|
356
|
+
) : (status === 'thinking' || status === 'tool') ? (
|
|
372
357
|
<>
|
|
373
358
|
<Box flexDirection="column" paddingX={1}>
|
|
374
359
|
<Text bold color="green">miii</Text>
|
|
375
|
-
{
|
|
376
|
-
|
|
377
|
-
|
|
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>
|
|
378
366
|
</Box>
|
|
379
367
|
<Divider cols={cols} />
|
|
380
368
|
</>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import type { Message } from '../../types.js'
|
|
4
4
|
|
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
cols: number
|
|
9
9
|
scrollOffset: number // 0 = pinned at bottom; N = N msgs hidden from bottom
|
|
10
10
|
streaming?: boolean
|
|
11
|
+
thinkingTick?: number
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// ─── height estimation ───────────────────────────────────────────────────────
|
|
@@ -104,7 +105,33 @@ function UserMsg({ msg }: { msg: Message }) {
|
|
|
104
105
|
)
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
const THINKING_PHRASES = [
|
|
109
|
+
'oh wow, a question. let me pretend to care…',
|
|
110
|
+
'consulting the void…',
|
|
111
|
+
'making something up, just a sec…',
|
|
112
|
+
'definitely not hallucinating right now…',
|
|
113
|
+
'running 47 mental tabs…',
|
|
114
|
+
'staring into the abyss (it blinked)…',
|
|
115
|
+
'calculating your fate, no pressure…',
|
|
116
|
+
'doing the thinking you pay me for…',
|
|
117
|
+
'processing your questionable life choices…',
|
|
118
|
+
'summoning coherent thoughts, rarely works…',
|
|
119
|
+
]
|
|
120
|
+
const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹']
|
|
121
|
+
|
|
122
|
+
function AssistantMsg({ msg, thinkingTick }: { msg: Message; thinkingTick?: number }) {
|
|
123
|
+
if (!msg.content && thinkingTick !== undefined) {
|
|
124
|
+
const phrase = THINKING_PHRASES[Math.floor(thinkingTick / 62) % THINKING_PHRASES.length]
|
|
125
|
+
const icon = SPARKLE[thinkingTick % SPARKLE.length]
|
|
126
|
+
return (
|
|
127
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
128
|
+
<Text bold color="green">miii</Text>
|
|
129
|
+
<Box paddingLeft={2}>
|
|
130
|
+
<Text color="yellow">{icon} </Text><Text color="gray" dimColor italic>{phrase}</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
108
135
|
return (
|
|
109
136
|
<Box flexDirection="column" marginBottom={1}>
|
|
110
137
|
<Text bold color="green">miii</Text>
|
|
@@ -139,10 +166,10 @@ function SystemMsg({ msg }: { msg: Message }) {
|
|
|
139
166
|
)
|
|
140
167
|
}
|
|
141
168
|
|
|
142
|
-
function MsgItem({ msg }: { msg: Message }) {
|
|
169
|
+
function MsgItem({ msg, thinkingTick }: { msg: Message; thinkingTick?: number }) {
|
|
143
170
|
switch (msg.role) {
|
|
144
171
|
case 'user': return <UserMsg msg={msg} />
|
|
145
|
-
case 'assistant': return <AssistantMsg msg={msg} />
|
|
172
|
+
case 'assistant': return <AssistantMsg msg={msg} thinkingTick={thinkingTick} />
|
|
146
173
|
case 'tool': return <ToolMsg msg={msg} />
|
|
147
174
|
case 'system': return <SystemMsg msg={msg} />
|
|
148
175
|
default: return null
|
|
@@ -165,7 +192,7 @@ function ScrollHint({ hiddenAbove, hiddenBelow }: { hiddenAbove: number; hiddenB
|
|
|
165
192
|
|
|
166
193
|
// ─── main export ─────────────────────────────────────────────────────────────
|
|
167
194
|
|
|
168
|
-
export function MessageList({ messages, rows, cols, scrollOffset, streaming }: Props) {
|
|
195
|
+
export function MessageList({ messages, rows, cols, scrollOffset, streaming, thinkingTick }: Props) {
|
|
169
196
|
const availRows = Math.max(rows - 2, 4)
|
|
170
197
|
const { visible, hiddenAbove, hiddenBelow } = useMemo(
|
|
171
198
|
() => computeSlice(messages, availRows, scrollOffset, cols),
|
|
@@ -182,7 +209,7 @@ export function MessageList({ messages, rows, cols, scrollOffset, streaming }: P
|
|
|
182
209
|
</Box>
|
|
183
210
|
)}
|
|
184
211
|
|
|
185
|
-
{visible.map(msg => <MsgItem key={msg.id} msg={msg} />)}
|
|
212
|
+
{visible.map(msg => <MsgItem key={msg.id} msg={msg} thinkingTick={thinkingTick} />)}
|
|
186
213
|
|
|
187
214
|
{streaming && scrollOffset === 0 && (
|
|
188
215
|
<Box paddingLeft={2}><Text color="gray" dimColor>▋</Text></Box>
|
|
@@ -17,7 +17,7 @@ export function StatusBar({ model, provider, status, tick }: Props) {
|
|
|
17
17
|
|
|
18
18
|
const statusNode =
|
|
19
19
|
status === 'idle' ? <Text color="green">● <Text color="gray">ready</Text></Text>
|
|
20
|
-
: status === '
|
|
20
|
+
: status === 'thinking' ? <Text color="yellow">{spinner} <Text color="gray">thinking</Text></Text>
|
|
21
21
|
: <Text color="yellow">{spinner} <Text color="gray">tool</Text></Text>
|
|
22
22
|
|
|
23
23
|
return (
|
package/src/tui/printer.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function welcome(provider: string, model: string, cwd: string): void {
|
|
|
90
90
|
row('', rcmd('/session', 'manage sessions')),
|
|
91
91
|
blank(),
|
|
92
92
|
row(` ${gray(provider + '/' + model)}`, ` ${bold(yellow('Tips'))}`),
|
|
93
|
-
row(` ${gray(shortCwd)}`, rcmd('ctrl+c', 'stop
|
|
93
|
+
row(` ${gray(shortCwd)}`, rcmd('ctrl+c', 'stop thinking')),
|
|
94
94
|
row('', rcmd('ctrl+c x2','exit')),
|
|
95
95
|
blank(),
|
|
96
96
|
bottom,
|
package/src/types.ts
CHANGED