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.
@@ -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,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('streaming')
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 stream({
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
- 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()) {
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
- ) : streamPreview ? (
356
+ ) : (status === 'thinking' || status === 'tool') ? (
372
357
  <>
373
358
  <Box flexDirection="column" paddingX={1}>
374
359
  <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
- ))}
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 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 (
@@ -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 streaming')),
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
@@ -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