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.
Files changed (39) hide show
  1. package/.claude/settings.local.json +10 -1
  2. package/dist/init.js +4 -0
  3. package/dist/init.js.map +1 -1
  4. package/dist/llm/ollama.d.ts +1 -0
  5. package/dist/llm/ollama.js +49 -0
  6. package/dist/llm/ollama.js.map +1 -1
  7. package/dist/llm/stream.d.ts +2 -3
  8. package/dist/llm/stream.js +22 -100
  9. package/dist/llm/stream.js.map +1 -1
  10. package/dist/parser/stream-parser.d.ts +4 -0
  11. package/dist/parser/stream-parser.js +170 -3
  12. package/dist/parser/stream-parser.js.map +1 -1
  13. package/dist/tools/index.js +42 -6
  14. package/dist/tools/index.js.map +1 -1
  15. package/dist/tui/App.js +62 -37
  16. package/dist/tui/App.js.map +1 -1
  17. package/dist/tui/InputBar.js +63 -37
  18. package/dist/tui/InputBar.js.map +1 -1
  19. package/dist/tui/components/InputArea.d.ts +2 -1
  20. package/dist/tui/components/InputArea.js +14 -4
  21. package/dist/tui/components/InputArea.js.map +1 -1
  22. package/dist/tui/components/MessageList.d.ts +2 -1
  23. package/dist/tui/components/MessageList.js +23 -5
  24. package/dist/tui/components/MessageList.js.map +1 -1
  25. package/dist/tui/components/StatusBar.js +1 -1
  26. package/dist/tui/components/StatusBar.js.map +1 -1
  27. package/dist/tui/printer.js +10 -2
  28. package/dist/tui/printer.js.map +1 -1
  29. package/dist/types.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/src/llm/stream.ts +18 -82
  32. package/src/parser/stream-parser.ts +145 -3
  33. package/src/tools/index.ts +40 -6
  34. package/src/tui/App.tsx +81 -36
  35. package/src/tui/InputBar.tsx +27 -42
  36. package/src/tui/components/MessageList.tsx +33 -6
  37. package/src/tui/components/StatusBar.tsx +1 -1
  38. package/src/tui/printer.ts +11 -2
  39. 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 { stream } from '../llm/stream.js'
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('streaming')
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 stream({
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
- onToken(token) {
133
- fullText += token
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
- async onDone() {
148
- if (tokenBufRef.current) {
149
- const flush = tokenBufRef.current
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) { setStatus('idle'); return }
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={status === '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}
@@ -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 { stream } from '../llm/stream.js'
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
- function lastLines(text: string, n: number): string[] {
29
- return text.split('\n').slice(-n)
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('streaming')
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 stream({
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
- onToken(token) {
120
- fullText += token
121
- tokenBufRef.current += token
122
- const now = Date.now()
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
- ) : streamPreview ? (
353
+ ) : (status === 'thinking' || status === 'tool') ? (
372
354
  <>
373
355
  <Box flexDirection="column" paddingX={1}>
374
356
  <Text bold color="green">miii</Text>
375
- {lastLines(streamPreview, PREVIEW_LINES).map((line, i) => (
376
- <Text key={i} color="gray" wrap="wrap">{line || ' '}</Text>
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 React, { useMemo } from 'react'
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
- function AssistantMsg({ msg }: { msg: Message }) {
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 === 'streaming' ? <Text color="yellow">{spinner} <Text color="gray">streaming</Text></Text>
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 (
@@ -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 streaming')),
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
@@ -1,5 +1,5 @@
1
1
  export type Role = 'user' | 'assistant' | 'system' | 'tool'
2
- export type Status = 'idle' | 'streaming' | 'tool'
2
+ export type Status = 'idle' | 'thinking' | 'tool'
3
3
 
4
4
  export interface Message {
5
5
  id: string