miii-cli 0.1.7 → 0.2.0
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 +10 -1
- package/dist/init.js +4 -0
- package/dist/init.js.map +1 -1
- package/dist/llm/ollama.d.ts +1 -0
- package/dist/llm/ollama.js +49 -0
- package/dist/llm/ollama.js.map +1 -1
- 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/parser/stream-parser.d.ts +4 -0
- package/dist/parser/stream-parser.js +170 -3
- package/dist/parser/stream-parser.js.map +1 -1
- package/dist/tools/index.js +42 -6
- package/dist/tools/index.js.map +1 -1
- package/dist/tui/App.js +62 -37
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/InputBar.js +63 -37
- package/dist/tui/InputBar.js.map +1 -1
- package/dist/tui/components/InputArea.d.ts +2 -1
- package/dist/tui/components/InputArea.js +14 -4
- package/dist/tui/components/InputArea.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 +10 -2
- 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/parser/stream-parser.ts +145 -3
- package/src/tools/index.ts +40 -6
- package/src/tui/App.tsx +81 -36
- package/src/tui/InputBar.tsx +27 -42
- package/src/tui/components/MessageList.tsx +33 -6
- package/src/tui/components/StatusBar.tsx +1 -1
- package/src/tui/printer.ts +11 -2
- package/src/types.ts +1 -1
package/src/tui/App.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
-
import { Box, useStdout, useInput } from 'ink'
|
|
2
|
+
import { Box, Text, useStdout, useInput } from 'ink'
|
|
3
3
|
import { StatusBar, Divider } from './components/StatusBar.js'
|
|
4
4
|
import { MessageList } from './components/MessageList.js'
|
|
5
5
|
import { InputArea } from './components/InputArea.js'
|
|
6
6
|
import { ModelPicker } from './components/ModelPicker.js'
|
|
7
|
-
import {
|
|
7
|
+
import { chat } from '../llm/stream.js'
|
|
8
8
|
import { listModels, pullModel } from '../llm/ollama.js'
|
|
9
9
|
import type { OllamaModel } from '../llm/ollama.js'
|
|
10
|
-
import { StreamParser } from '../parser/stream-parser.js'
|
|
10
|
+
import { StreamParser, extractBareToolCall } from '../parser/stream-parser.js'
|
|
11
11
|
import { tools, getSystemPrompt } from '../tools/index.js'
|
|
12
12
|
import { readFile, guardPath } from '../files/ops.js'
|
|
13
13
|
import type { SkillLoader } from '../skills/loader.js'
|
|
@@ -21,7 +21,6 @@ interface Props {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const MAX_TOOL_DEPTH = 6
|
|
24
|
-
const RENDER_THROTTLE_MS = 40
|
|
25
24
|
|
|
26
25
|
function expandAtRefs(text: string, cwd: string): { displayText: string; contextPrefix: string } {
|
|
27
26
|
const refs = [...text.matchAll(/@([\w./\-]+)/g)]
|
|
@@ -63,13 +62,19 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
63
62
|
const currentModelRef = useRef(currentModel)
|
|
64
63
|
const abortRef = useRef<AbortController | null>(null)
|
|
65
64
|
const pullAbortRef = useRef<AbortController | null>(null)
|
|
66
|
-
const tokenBufRef = useRef('')
|
|
67
|
-
const lastRenderRef = useRef(0)
|
|
68
65
|
const messagesRef = useRef(messages)
|
|
66
|
+
const approvalResolveRef = useRef<((ok: boolean) => void) | null>(null)
|
|
67
|
+
const [pendingApproval, setPendingApproval] = useState<{
|
|
68
|
+
toolName: string
|
|
69
|
+
path: string
|
|
70
|
+
content?: string
|
|
71
|
+
} | null>(null)
|
|
72
|
+
const pendingApprovalRef = useRef(pendingApproval)
|
|
69
73
|
|
|
70
74
|
useEffect(() => { systemPromptRef.current = systemPrompt }, [systemPrompt])
|
|
71
75
|
useEffect(() => { currentModelRef.current = currentModel }, [currentModel])
|
|
72
76
|
useEffect(() => { messagesRef.current = messages }, [messages])
|
|
77
|
+
useEffect(() => { pendingApprovalRef.current = pendingApproval }, [pendingApproval])
|
|
73
78
|
|
|
74
79
|
useEffect(() => {
|
|
75
80
|
if (status === 'idle') return
|
|
@@ -80,6 +85,20 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
80
85
|
// Scroll keybindings — PageUp/PageDn scroll message history
|
|
81
86
|
const SCROLL_STEP = 5
|
|
82
87
|
useInput((_input, key) => {
|
|
88
|
+
// approvalResolveRef is set synchronously in requestApproval — no useEffect needed
|
|
89
|
+
if (approvalResolveRef.current) {
|
|
90
|
+
const resolve = approvalResolveRef.current
|
|
91
|
+
if (_input === 'y' || _input === 'Y') {
|
|
92
|
+
approvalResolveRef.current = null
|
|
93
|
+
setPendingApproval(null)
|
|
94
|
+
resolve(true)
|
|
95
|
+
} else if (_input === 'n' || _input === 'N' || key.escape) {
|
|
96
|
+
approvalResolveRef.current = null
|
|
97
|
+
setPendingApproval(null)
|
|
98
|
+
resolve(false)
|
|
99
|
+
}
|
|
100
|
+
return
|
|
101
|
+
}
|
|
83
102
|
if (pickerOpen) return
|
|
84
103
|
if (key.pageUp) {
|
|
85
104
|
setScrollOffset(n => Math.min(n + SCROLL_STEP, Math.max(0, messages.length - 1)))
|
|
@@ -92,6 +111,19 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
92
111
|
const cols = stdout.columns ?? 80
|
|
93
112
|
const rows = stdout.rows ?? 24
|
|
94
113
|
|
|
114
|
+
const APPROVAL_TOOLS = new Set(['delete_file'])
|
|
115
|
+
|
|
116
|
+
const requestApproval = useCallback((toolName: string, args: Record<string, unknown>): Promise<boolean> => {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
approvalResolveRef.current = resolve
|
|
119
|
+
setPendingApproval({
|
|
120
|
+
toolName,
|
|
121
|
+
path: ((args.path ?? args.from) as string) ?? '',
|
|
122
|
+
content: args.content as string | undefined,
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
95
127
|
function addMsg(role: Message['role'], content: string, id?: string): string {
|
|
96
128
|
const mid = id ?? generateId()
|
|
97
129
|
setMessages(prev => [...prev, { id: mid, role, content, timestamp: Date.now() }])
|
|
@@ -110,18 +142,14 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
110
142
|
|
|
111
143
|
const runLoop = useCallback(async (contextMsgs: ChatMessage[], depth = 0) => {
|
|
112
144
|
if (depth >= MAX_TOOL_DEPTH) { setStatus('idle'); return }
|
|
113
|
-
setStatus('
|
|
145
|
+
setStatus('thinking')
|
|
114
146
|
|
|
115
147
|
const assistantId = generateId()
|
|
116
148
|
setMessages(prev => [...prev, { id: assistantId, role: 'assistant', content: '', timestamp: Date.now() }])
|
|
117
149
|
|
|
118
|
-
const parser = new StreamParser()
|
|
119
|
-
const pendingTools: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
120
|
-
let fullText = ''
|
|
121
|
-
|
|
122
150
|
abortRef.current = new AbortController()
|
|
123
151
|
|
|
124
|
-
await
|
|
152
|
+
await chat({
|
|
125
153
|
provider: config.provider,
|
|
126
154
|
model: currentModelRef.current,
|
|
127
155
|
baseUrl: config.baseUrl,
|
|
@@ -129,32 +157,27 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
129
157
|
messages: contextMsgs,
|
|
130
158
|
signal: abortRef.current.signal,
|
|
131
159
|
|
|
132
|
-
|
|
133
|
-
fullText
|
|
134
|
-
tokenBufRef.current += token
|
|
135
|
-
const now = Date.now()
|
|
136
|
-
if (now - lastRenderRef.current >= RENDER_THROTTLE_MS) {
|
|
137
|
-
const flush = tokenBufRef.current
|
|
138
|
-
tokenBufRef.current = ''
|
|
139
|
-
lastRenderRef.current = now
|
|
140
|
-
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: m.content + flush } : m))
|
|
141
|
-
}
|
|
142
|
-
for (const item of parser.feed(token)) {
|
|
143
|
-
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
144
|
-
}
|
|
145
|
-
},
|
|
160
|
+
async onDone(fullText) {
|
|
161
|
+
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: fullText } : m))
|
|
146
162
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
tokenBufRef.current = ''
|
|
151
|
-
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: m.content + flush } : m))
|
|
152
|
-
}
|
|
153
|
-
for (const item of parser.flush()) {
|
|
163
|
+
const pendingTools: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
164
|
+
const parser = new StreamParser()
|
|
165
|
+
for (const item of [...parser.feed(fullText), ...parser.flush()]) {
|
|
154
166
|
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
if (!pendingTools.length) {
|
|
169
|
+
if (!pendingTools.length) {
|
|
170
|
+
const bare = extractBareToolCall(fullText)
|
|
171
|
+
if (bare) {
|
|
172
|
+
pendingTools.push(bare)
|
|
173
|
+
} else {
|
|
174
|
+
if (fullText.includes('{"name"')) {
|
|
175
|
+
addMsg('tool', 'tool_call parse failed — could not extract tool call from model output')
|
|
176
|
+
}
|
|
177
|
+
setStatus('idle')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
}
|
|
158
181
|
|
|
159
182
|
setStatus('tool')
|
|
160
183
|
const next: ChatMessage[] = [...contextMsgs, { role: 'assistant', content: fullText }]
|
|
@@ -163,6 +186,15 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
163
186
|
const tool = tools.find(t => t.name === tc.name)
|
|
164
187
|
const toolId = generateId()
|
|
165
188
|
if (tool) {
|
|
189
|
+
if (APPROVAL_TOOLS.has(tc.name)) {
|
|
190
|
+
const approved = await requestApproval(tc.name, tc.args)
|
|
191
|
+
if (!approved) {
|
|
192
|
+
const cancelled = `[${tc.name}] cancelled by user`
|
|
193
|
+
setMessages(prev => [...prev, { id: toolId, role: 'tool', content: cancelled, timestamp: Date.now() }])
|
|
194
|
+
next.push({ role: 'user', content: `Tool ${tc.name} was cancelled by user.` })
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
}
|
|
166
198
|
try {
|
|
167
199
|
const result = await tool.execute(tc.args)
|
|
168
200
|
setMessages(prev => [...prev, { id: toolId, role: 'tool', content: `[${tc.name}]\n${result}`, timestamp: Date.now() }])
|
|
@@ -183,6 +215,7 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
183
215
|
},
|
|
184
216
|
|
|
185
217
|
onError(err) {
|
|
218
|
+
setMessages(prev => prev.filter(m => m.id !== assistantId))
|
|
186
219
|
addMsg('system', `error: ${err.message}`)
|
|
187
220
|
setStatus('idle')
|
|
188
221
|
},
|
|
@@ -268,7 +301,6 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
268
301
|
const handleAbort = useCallback(() => {
|
|
269
302
|
abortRef.current?.abort()
|
|
270
303
|
setStatus('idle')
|
|
271
|
-
tokenBufRef.current = ''
|
|
272
304
|
}, [])
|
|
273
305
|
|
|
274
306
|
const skillList = skills.list()
|
|
@@ -294,10 +326,23 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
294
326
|
rows={rows - 8}
|
|
295
327
|
cols={cols}
|
|
296
328
|
scrollOffset={scrollOffset}
|
|
297
|
-
streaming={
|
|
329
|
+
streaming={false}
|
|
330
|
+
thinkingTick={status === 'thinking' ? tick : undefined}
|
|
298
331
|
/>
|
|
299
332
|
)}
|
|
300
333
|
<Divider cols={cols} />
|
|
334
|
+
{pendingApproval && (
|
|
335
|
+
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1} marginBottom={1}>
|
|
336
|
+
<Text color="yellow" bold>Allow {pendingApproval.toolName}?</Text>
|
|
337
|
+
<Text> path: <Text color="cyan">{pendingApproval.path}</Text></Text>
|
|
338
|
+
{pendingApproval.content && (
|
|
339
|
+
<Text color="gray" dimColor>
|
|
340
|
+
{pendingApproval.content.split('\n').slice(0, 12).join('\n')}
|
|
341
|
+
</Text>
|
|
342
|
+
)}
|
|
343
|
+
<Text color="green">[y] approve <Text color="red">[n] cancel</Text></Text>
|
|
344
|
+
</Box>
|
|
345
|
+
)}
|
|
301
346
|
<InputArea
|
|
302
347
|
status={status}
|
|
303
348
|
skills={skillList}
|
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,7 +57,6 @@ 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)
|
|
54
61
|
const [planningMode, setPlanningMode] = useState(false)
|
|
55
62
|
|
|
@@ -62,8 +69,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
62
69
|
|
|
63
70
|
const abortRef = useRef<AbortController | null>(null)
|
|
64
71
|
const pullAbortRef = useRef<AbortController | null>(null)
|
|
65
|
-
const tokenBufRef = useRef('')
|
|
66
|
-
const lastRenderRef = useRef(0)
|
|
67
72
|
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`))
|
|
68
73
|
const currentModelRef = useRef(currentModel)
|
|
69
74
|
const sessionNameRef = useRef(sessionName)
|
|
@@ -100,45 +105,25 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
100
105
|
|
|
101
106
|
const runLoop = useCallback(async (contextMsgs: ChatMessage[], depth = 0) => {
|
|
102
107
|
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 = ''
|
|
108
|
+
setStatus('thinking')
|
|
109
109
|
|
|
110
110
|
abortRef.current = new AbortController()
|
|
111
111
|
|
|
112
|
-
await
|
|
112
|
+
await chat({
|
|
113
113
|
provider: config.provider,
|
|
114
114
|
model: currentModelRef.current,
|
|
115
115
|
baseUrl: config.baseUrl,
|
|
116
116
|
messages: contextMsgs,
|
|
117
117
|
signal: abortRef.current.signal,
|
|
118
118
|
|
|
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()) {
|
|
119
|
+
async onDone(fullText) {
|
|
120
|
+
const pendingTools: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
121
|
+
const parser = new StreamParser()
|
|
122
|
+
for (const item of [...parser.feed(fullText), ...parser.flush()]) {
|
|
136
123
|
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
137
124
|
}
|
|
138
125
|
|
|
139
126
|
printer.assistantMsg(fullText)
|
|
140
|
-
setStreamPreview('')
|
|
141
|
-
|
|
142
127
|
historyRef.current.push({ role: 'assistant', content: fullText })
|
|
143
128
|
saveSession(sessionNameRef.current, historyRef.current)
|
|
144
129
|
|
|
@@ -171,7 +156,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
171
156
|
onError(err) {
|
|
172
157
|
if (err.name !== 'AbortError') printer.errorMsg(err.message)
|
|
173
158
|
setStatus('idle')
|
|
174
|
-
setStreamPreview('')
|
|
175
159
|
},
|
|
176
160
|
})
|
|
177
161
|
}, [config])
|
|
@@ -344,8 +328,6 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
344
328
|
const handleAbort = useCallback(() => {
|
|
345
329
|
abortRef.current?.abort()
|
|
346
330
|
setStatus('idle')
|
|
347
|
-
setStreamPreview('')
|
|
348
|
-
tokenBufRef.current = ''
|
|
349
331
|
}, [])
|
|
350
332
|
|
|
351
333
|
const skillList = skills.list()
|
|
@@ -368,13 +350,16 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
368
350
|
/>
|
|
369
351
|
<Divider cols={cols} />
|
|
370
352
|
</>
|
|
371
|
-
) :
|
|
353
|
+
) : (status === 'thinking' || status === 'tool') ? (
|
|
372
354
|
<>
|
|
373
355
|
<Box flexDirection="column" paddingX={1}>
|
|
374
356
|
<Text bold color="green">miii</Text>
|
|
375
|
-
{
|
|
376
|
-
|
|
377
|
-
|
|
357
|
+
<Box paddingLeft={2}>
|
|
358
|
+
{status === 'thinking'
|
|
359
|
+
? <><Text color="yellow">{SPARKLE[tick % SPARKLE.length]} </Text><Text color="gray" dimColor italic>{THINKING_PHRASES[Math.floor(tick / 62) % THINKING_PHRASES.length]}</Text></>
|
|
360
|
+
: <Text color="yellow" dimColor>running tool…</Text>
|
|
361
|
+
}
|
|
362
|
+
</Box>
|
|
378
363
|
</Box>
|
|
379
364
|
<Divider cols={cols} />
|
|
380
365
|
</>
|
|
@@ -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
|
@@ -18,6 +18,15 @@ function indent(text: string, pad = ' '): string {
|
|
|
18
18
|
return text.split('\n').map(l => pad + l).join('\n')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function stripMarkdown(s: string): string {
|
|
22
|
+
return s
|
|
23
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
24
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
25
|
+
.replace(/\*(.+?)\*/g, '$1')
|
|
26
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
27
|
+
.replace(/^#{1,6} /gm, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function formatContent(text: string): string {
|
|
22
31
|
const lines = text.split('\n')
|
|
23
32
|
let inCode = false
|
|
@@ -30,7 +39,7 @@ function formatContent(text: string): string {
|
|
|
30
39
|
} else if (inCode) {
|
|
31
40
|
out.push(' ' + yellow(line || ' '))
|
|
32
41
|
} else {
|
|
33
|
-
out.push(' ' + (line || ''))
|
|
42
|
+
out.push(' ' + stripMarkdown(line || ''))
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
45
|
return out.join('\n')
|
|
@@ -81,7 +90,7 @@ export function welcome(provider: string, model: string, cwd: string): void {
|
|
|
81
90
|
row('', rcmd('/session', 'manage sessions')),
|
|
82
91
|
blank(),
|
|
83
92
|
row(` ${gray(provider + '/' + model)}`, ` ${bold(yellow('Tips'))}`),
|
|
84
|
-
row(` ${gray(shortCwd)}`, rcmd('ctrl+c', 'stop
|
|
93
|
+
row(` ${gray(shortCwd)}`, rcmd('ctrl+c', 'stop thinking')),
|
|
85
94
|
row('', rcmd('ctrl+c x2','exit')),
|
|
86
95
|
blank(),
|
|
87
96
|
bottom,
|
package/src/types.ts
CHANGED