miii-cli 0.1.7 → 0.1.8

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/src/tui/App.tsx CHANGED
@@ -1,5 +1,5 @@
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'
@@ -7,7 +7,7 @@ import { ModelPicker } from './components/ModelPicker.js'
7
7
  import { stream } 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'
@@ -66,10 +66,18 @@ export function App({ config, skills, cwd }: Props) {
66
66
  const tokenBufRef = useRef('')
67
67
  const lastRenderRef = useRef(0)
68
68
  const messagesRef = useRef(messages)
69
+ const approvalResolveRef = useRef<((ok: boolean) => void) | null>(null)
70
+ const [pendingApproval, setPendingApproval] = useState<{
71
+ toolName: string
72
+ path: string
73
+ content?: string
74
+ } | null>(null)
75
+ const pendingApprovalRef = useRef(pendingApproval)
69
76
 
70
77
  useEffect(() => { systemPromptRef.current = systemPrompt }, [systemPrompt])
71
78
  useEffect(() => { currentModelRef.current = currentModel }, [currentModel])
72
79
  useEffect(() => { messagesRef.current = messages }, [messages])
80
+ useEffect(() => { pendingApprovalRef.current = pendingApproval }, [pendingApproval])
73
81
 
74
82
  useEffect(() => {
75
83
  if (status === 'idle') return
@@ -80,6 +88,20 @@ export function App({ config, skills, cwd }: Props) {
80
88
  // Scroll keybindings — PageUp/PageDn scroll message history
81
89
  const SCROLL_STEP = 5
82
90
  useInput((_input, key) => {
91
+ // approvalResolveRef is set synchronously in requestApproval — no useEffect needed
92
+ if (approvalResolveRef.current) {
93
+ const resolve = approvalResolveRef.current
94
+ if (_input === 'y' || _input === 'Y') {
95
+ approvalResolveRef.current = null
96
+ setPendingApproval(null)
97
+ resolve(true)
98
+ } else if (_input === 'n' || _input === 'N' || key.escape) {
99
+ approvalResolveRef.current = null
100
+ setPendingApproval(null)
101
+ resolve(false)
102
+ }
103
+ return
104
+ }
83
105
  if (pickerOpen) return
84
106
  if (key.pageUp) {
85
107
  setScrollOffset(n => Math.min(n + SCROLL_STEP, Math.max(0, messages.length - 1)))
@@ -92,6 +114,19 @@ export function App({ config, skills, cwd }: Props) {
92
114
  const cols = stdout.columns ?? 80
93
115
  const rows = stdout.rows ?? 24
94
116
 
117
+ const APPROVAL_TOOLS = new Set(['delete_file'])
118
+
119
+ const requestApproval = useCallback((toolName: string, args: Record<string, unknown>): Promise<boolean> => {
120
+ return new Promise((resolve) => {
121
+ approvalResolveRef.current = resolve
122
+ setPendingApproval({
123
+ toolName,
124
+ path: ((args.path ?? args.from) as string) ?? '',
125
+ content: args.content as string | undefined,
126
+ })
127
+ })
128
+ }, [])
129
+
95
130
  function addMsg(role: Message['role'], content: string, id?: string): string {
96
131
  const mid = id ?? generateId()
97
132
  setMessages(prev => [...prev, { id: mid, role, content, timestamp: Date.now() }])
@@ -154,7 +189,19 @@ export function App({ config, skills, cwd }: Props) {
154
189
  if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
155
190
  }
156
191
 
157
- if (!pendingTools.length) { setStatus('idle'); return }
192
+ if (!pendingTools.length) {
193
+ // Fallback: LLM emitted bare JSON without <tool_call> wrapper
194
+ const bare = extractBareToolCall(fullText)
195
+ if (bare) {
196
+ pendingTools.push(bare)
197
+ } else {
198
+ if (fullText.includes('{"name"')) {
199
+ addMsg('tool', 'tool_call parse failed — could not extract tool call from model output')
200
+ }
201
+ setStatus('idle')
202
+ return
203
+ }
204
+ }
158
205
 
159
206
  setStatus('tool')
160
207
  const next: ChatMessage[] = [...contextMsgs, { role: 'assistant', content: fullText }]
@@ -163,6 +210,15 @@ export function App({ config, skills, cwd }: Props) {
163
210
  const tool = tools.find(t => t.name === tc.name)
164
211
  const toolId = generateId()
165
212
  if (tool) {
213
+ if (APPROVAL_TOOLS.has(tc.name)) {
214
+ const approved = await requestApproval(tc.name, tc.args)
215
+ if (!approved) {
216
+ const cancelled = `[${tc.name}] cancelled by user`
217
+ setMessages(prev => [...prev, { id: toolId, role: 'tool', content: cancelled, timestamp: Date.now() }])
218
+ next.push({ role: 'user', content: `Tool ${tc.name} was cancelled by user.` })
219
+ continue
220
+ }
221
+ }
166
222
  try {
167
223
  const result = await tool.execute(tc.args)
168
224
  setMessages(prev => [...prev, { id: toolId, role: 'tool', content: `[${tc.name}]\n${result}`, timestamp: Date.now() }])
@@ -298,6 +354,18 @@ export function App({ config, skills, cwd }: Props) {
298
354
  />
299
355
  )}
300
356
  <Divider cols={cols} />
357
+ {pendingApproval && (
358
+ <Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1} marginBottom={1}>
359
+ <Text color="yellow" bold>Allow {pendingApproval.toolName}?</Text>
360
+ <Text> path: <Text color="cyan">{pendingApproval.path}</Text></Text>
361
+ {pendingApproval.content && (
362
+ <Text color="gray" dimColor>
363
+ {pendingApproval.content.split('\n').slice(0, 12).join('\n')}
364
+ </Text>
365
+ )}
366
+ <Text color="green">[y] approve <Text color="red">[n] cancel</Text></Text>
367
+ </Box>
368
+ )}
301
369
  <InputArea
302
370
  status={status}
303
371
  skills={skillList}
@@ -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')