@miphamai/cli 0.59.0 → 0.60.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/package.json +1 -1
- package/src/commands/autoloop-journal.ts +42 -2
- package/src/config/defaults.ts +3 -0
- package/src/core/engine.ts +65 -2
- package/src/core/permission.ts +45 -0
- package/src/i18n-core/locales/en-US.json +4 -0
- package/src/i18n-core/locales/zh-CN.json +4 -0
- package/src/providers/openai-compat.ts +1 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +11 -0
- package/src/tools/computer/app-launcher.ts +7 -1
- package/src/tools/computer/screenshot.ts +7 -1
- package/src/tools/scheduling/schedule-wakeup.ts +15 -0
- package/src/ui/app.tsx +349 -219
- package/src/ui/command-picker.tsx +20 -13
- package/src/ui/command-token.ts +10 -0
- package/src/ui/commands.ts +22 -7
- package/src/ui/input.tsx +11 -3
- package/src/ui/loop-noop.ts +23 -0
- package/src/ui/thinking.ts +21 -0
|
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from 'ink'
|
|
|
3
3
|
import TextInput from 'ink-text-input'
|
|
4
4
|
import { useI18n } from '../i18n-context'
|
|
5
5
|
import { getCommandList } from './commands.js'
|
|
6
|
+
import { commandToken, hasInlineArgs } from './command-token.js'
|
|
6
7
|
|
|
7
8
|
interface CommandPickerProps {
|
|
8
9
|
/** Text already typed (e.g. "/", "/age") — used as initial filter */
|
|
@@ -60,9 +61,10 @@ export function CommandPicker({
|
|
|
60
61
|
const [filter, setFilter] = useState(initialFilter)
|
|
61
62
|
const [cursorIdx, setCursorIdx] = useState(0)
|
|
62
63
|
|
|
63
|
-
// Filter commands based on user input
|
|
64
|
+
// Filter commands based on user input (match on the command-name token, not the
|
|
65
|
+
// full line — so "/loop 60s echo hello" still matches "/loop").
|
|
64
66
|
const filtered = useMemo(() => {
|
|
65
|
-
const q =
|
|
67
|
+
const q = commandToken(filter)
|
|
66
68
|
if (!q) return allCommands
|
|
67
69
|
return allCommands.filter(
|
|
68
70
|
(cmd) => cmd.name.toLowerCase().includes(q) || cmd.description.toLowerCase().includes(q),
|
|
@@ -86,6 +88,19 @@ export function CommandPicker({
|
|
|
86
88
|
const safeCursor = (i: number) =>
|
|
87
89
|
filtered.length === 0 ? 0 : ((i % filtered.length) + filtered.length) % filtered.length
|
|
88
90
|
|
|
91
|
+
// Submit the selection: if the filter has inline args (e.g. "/loop 60s echo hello"),
|
|
92
|
+
// submit the full line so the args reach the command handler; otherwise submit the
|
|
93
|
+
// highlighted command name.
|
|
94
|
+
const submitSelection = () => {
|
|
95
|
+
const trimmed = filter.trim()
|
|
96
|
+
if (hasInlineArgs(trimmed)) {
|
|
97
|
+
onSelect(trimmed)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const selected = filtered[cursorIdx]
|
|
101
|
+
if (selected) onSelect(selected.name)
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
useInput((_input, key) => {
|
|
90
105
|
if (key.escape) {
|
|
91
106
|
onClose()
|
|
@@ -93,10 +108,7 @@ export function CommandPicker({
|
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
if (key.return) {
|
|
96
|
-
|
|
97
|
-
if (selected) {
|
|
98
|
-
onSelect(selected.name)
|
|
99
|
-
}
|
|
111
|
+
submitSelection()
|
|
100
112
|
return
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -132,7 +144,7 @@ export function CommandPicker({
|
|
|
132
144
|
{visible.map((cmd, i) => {
|
|
133
145
|
const globalIdx = i + scrollStart
|
|
134
146
|
const isCursor = globalIdx === cursorIdx
|
|
135
|
-
const query =
|
|
147
|
+
const query = commandToken(filter)
|
|
136
148
|
const segments = highlightMatches(cmd.name, query)
|
|
137
149
|
const padLen = Math.max(0, 20 - cmd.name.length)
|
|
138
150
|
return (
|
|
@@ -171,12 +183,7 @@ export function CommandPicker({
|
|
|
171
183
|
<TextInput
|
|
172
184
|
value={filter.startsWith('/') ? filter.slice(1) : filter}
|
|
173
185
|
onChange={(val) => setFilter(`/${val}`)}
|
|
174
|
-
onSubmit={() =>
|
|
175
|
-
const selected = filtered[cursorIdx]
|
|
176
|
-
if (selected) {
|
|
177
|
-
onSelect(selected.name)
|
|
178
|
-
}
|
|
179
|
-
}}
|
|
186
|
+
onSubmit={() => submitSelection()}
|
|
180
187
|
placeholder={t('ui.command_picker.placeholder')}
|
|
181
188
|
/>
|
|
182
189
|
</Box>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Extract the command-name token from a slash filter: "/loop 60s echo hello" → "loop". */
|
|
2
|
+
export function commandToken(filter: string): string {
|
|
3
|
+
const withoutSlash = filter.startsWith('/') ? filter.slice(1) : filter
|
|
4
|
+
return withoutSlash.trim().split(/\s+/)[0]?.toLowerCase() ?? ''
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** True when the filter carries inline args beyond the command name: "/loop auto" → true, "/loop" → false. */
|
|
8
|
+
export function hasInlineArgs(filter: string): boolean {
|
|
9
|
+
return filter.trim().includes(' ')
|
|
10
|
+
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -62,6 +62,7 @@ import { commitCmd, pushCmd, prCmd, issueCmd } from '../commands/git.js'
|
|
|
62
62
|
import { suggestDirectories } from '../commands/cd-suggest.js'
|
|
63
63
|
import { keysCmd } from '../commands/keys'
|
|
64
64
|
import { workflowViewCmd, workflowWatchCmd } from '../commands/workflow-view.js'
|
|
65
|
+
import { listActiveAutoloops, formatLoopRows } from '../commands/autoloop-journal.js'
|
|
65
66
|
import { execSync } from 'node:child_process'
|
|
66
67
|
import { OLLAMA_PRESET_MODELS } from '../shared/constants'
|
|
67
68
|
import { renameActiveSession } from '../agent/cross-session/discovery'
|
|
@@ -545,6 +546,9 @@ const configCmd: CommandHandler = (ctx) => {
|
|
|
545
546
|
`defaultProvider: ${c.defaultProvider}`,
|
|
546
547
|
`defaultModel: ${c.defaultModel}`,
|
|
547
548
|
`permission: ${c.permission}`,
|
|
549
|
+
`showThinking: ${c.showThinking ?? 'off'}`,
|
|
550
|
+
`showSchedulingNotices: ${c.showSchedulingNotices ?? false}`,
|
|
551
|
+
`showCommandPicker: ${c.showCommandPicker ?? false}`,
|
|
548
552
|
`providers: ${c.providers.length} configured`,
|
|
549
553
|
'',
|
|
550
554
|
`crossSessionInbound: ${xs.crossSessionInbound}`,
|
|
@@ -2105,6 +2109,12 @@ const usageCmd: CommandHandler = (ctx) => {
|
|
|
2105
2109
|
const toolSection =
|
|
2106
2110
|
toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
|
|
2107
2111
|
|
|
2112
|
+
const loops = listActiveAutoloops()
|
|
2113
|
+
const loopSection =
|
|
2114
|
+
loops.length > 0
|
|
2115
|
+
? `\n${t('commands.usage.loops_header')}\n${formatLoopRows(loops).join('\n')}`
|
|
2116
|
+
: ''
|
|
2117
|
+
|
|
2108
2118
|
return {
|
|
2109
2119
|
content: stripIndent`
|
|
2110
2120
|
${t('commands.usage.title')}
|
|
@@ -2115,7 +2125,7 @@ const usageCmd: CommandHandler = (ctx) => {
|
|
|
2115
2125
|
${t('commands.usage.model_line', { model: ctx.modelId })}
|
|
2116
2126
|
|
|
2117
2127
|
${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
|
|
2118
|
-
${toolSection}
|
|
2128
|
+
${toolSection}${loopSection}
|
|
2119
2129
|
|
|
2120
2130
|
${t('commands.usage.hint')}
|
|
2121
2131
|
`,
|
|
@@ -2618,7 +2628,7 @@ function parseInterval(raw: string): number | null {
|
|
|
2618
2628
|
return null
|
|
2619
2629
|
}
|
|
2620
2630
|
|
|
2621
|
-
const loopCmd: CommandHandler = async (
|
|
2631
|
+
const loopCmd: CommandHandler = async (ctx, args) => {
|
|
2622
2632
|
const sub = args[0]
|
|
2623
2633
|
|
|
2624
2634
|
// ── /loop init — scaffold project vault ──
|
|
@@ -2720,7 +2730,7 @@ const loopCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2720
2730
|
|
|
2721
2731
|
const sessionId = `autoloop-${Date.now()}`
|
|
2722
2732
|
const { createAutoloopJournal } = await import('../commands/autoloop-journal.js')
|
|
2723
|
-
createAutoloopJournal(sessionId, prompt)
|
|
2733
|
+
createAutoloopJournal(sessionId, prompt, ctx.engine.getUsageTracker().totalApiTokens)
|
|
2724
2734
|
|
|
2725
2735
|
const autoPrompt =
|
|
2726
2736
|
`## Autonomous Loop — ${sessionId}\n\n` +
|
|
@@ -2780,7 +2790,7 @@ const loopCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2780
2790
|
if (fullPrompt.length > 0) {
|
|
2781
2791
|
const sessionId = `autoloop-${Date.now()}`
|
|
2782
2792
|
const { createAutoloopJournal } = await import('../commands/autoloop-journal.js')
|
|
2783
|
-
createAutoloopJournal(sessionId, fullPrompt)
|
|
2793
|
+
createAutoloopJournal(sessionId, fullPrompt, ctx.engine.getUsageTracker().totalApiTokens)
|
|
2784
2794
|
|
|
2785
2795
|
const autoPrompt =
|
|
2786
2796
|
`## Autonomous Loop — ${sessionId}\n\n` +
|
|
@@ -2813,9 +2823,14 @@ const loopCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2813
2823
|
{ delaySeconds: seconds, reason: `Loop: ${prompt.slice(0, 40)}`, prompt },
|
|
2814
2824
|
{ cwd: process.cwd(), sessionId: 'loop-session', provider: '', model: '' },
|
|
2815
2825
|
)
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2826
|
+
if (!result.success) {
|
|
2827
|
+
return { content: `Loop failed: ${result.error || 'unknown error'}` }
|
|
2828
|
+
}
|
|
2829
|
+
// When scheduling notices are hidden, stay silent — the loop is already scheduled.
|
|
2830
|
+
if (ctx.config.showSchedulingNotices === false) {
|
|
2831
|
+
return { content: '' }
|
|
2832
|
+
}
|
|
2833
|
+
return { content: result.content }
|
|
2819
2834
|
} catch (e) {
|
|
2820
2835
|
return { content: `Loop failed: ${e instanceof Error ? e.message : String(e)}` }
|
|
2821
2836
|
}
|
package/src/ui/input.tsx
CHANGED
|
@@ -21,6 +21,8 @@ interface InputBarProps {
|
|
|
21
21
|
onCyclePermission?: () => void
|
|
22
22
|
/** Escape → cancel loading / clear draft */
|
|
23
23
|
onCancel?: () => void
|
|
24
|
+
/** When false, don't auto-open the slash-command picker when typing `/`. */
|
|
25
|
+
showCommandPicker?: boolean
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// ── Loading verb keys (i18n) ──
|
|
@@ -75,6 +77,11 @@ export function isBulkInput(prev: string, next: string): boolean {
|
|
|
75
77
|
return Math.abs(next.length - prev.length) > 1
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/** True when typing a leading `/` should auto-open the slash-command picker. */
|
|
81
|
+
export function shouldAutoOpenPicker(value: string, prevValue: string, enabled: boolean): boolean {
|
|
82
|
+
return enabled && value.startsWith('/') && !prevValue.startsWith('/')
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
export function InputBar({
|
|
79
86
|
onSubmit,
|
|
80
87
|
isLoading,
|
|
@@ -84,6 +91,7 @@ export function InputBar({
|
|
|
84
91
|
onToggleAgentView,
|
|
85
92
|
onCyclePermission,
|
|
86
93
|
onCancel,
|
|
94
|
+
showCommandPicker = true,
|
|
87
95
|
}: InputBarProps) {
|
|
88
96
|
const { t } = useI18n()
|
|
89
97
|
const [value, setValue] = useState('')
|
|
@@ -251,9 +259,9 @@ export function InputBar({
|
|
|
251
259
|
const [pickerActive, setPickerActive] = useState(false)
|
|
252
260
|
const prevValueRef = useRef(value)
|
|
253
261
|
|
|
254
|
-
// Auto-activate picker when user types "/"
|
|
262
|
+
// Auto-activate picker when user types "/" (unless disabled via showCommandPicker)
|
|
255
263
|
useEffect(() => {
|
|
256
|
-
if (value
|
|
264
|
+
if (shouldAutoOpenPicker(value, prevValueRef.current, showCommandPicker)) {
|
|
257
265
|
setPickerActive(true)
|
|
258
266
|
}
|
|
259
267
|
// Dismiss picker when user clears the / prefix
|
|
@@ -261,7 +269,7 @@ export function InputBar({
|
|
|
261
269
|
setPickerActive(false)
|
|
262
270
|
}
|
|
263
271
|
prevValueRef.current = value
|
|
264
|
-
}, [value])
|
|
272
|
+
}, [value, showCommandPicker])
|
|
265
273
|
|
|
266
274
|
// Keep valueRef in sync with state (so useInput handlers read latest value)
|
|
267
275
|
useEffect(() => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /loop idle-wakeup folding — collapse consecutive noop ticks into a single
|
|
3
|
+
* "💤 idle ×N" line so repeated "nothing to report" wakeups don't spam the UI.
|
|
4
|
+
*/
|
|
5
|
+
export function collapseNoopTicks(ticks: Array<{ noop: boolean }>): string {
|
|
6
|
+
const lines: string[] = []
|
|
7
|
+
let i = 0
|
|
8
|
+
while (i < ticks.length) {
|
|
9
|
+
const tick = ticks[i]!
|
|
10
|
+
if (tick.noop) {
|
|
11
|
+
let count = 0
|
|
12
|
+
while (i < ticks.length && ticks[i]!.noop) {
|
|
13
|
+
count++
|
|
14
|
+
i++
|
|
15
|
+
}
|
|
16
|
+
lines.push(`💤 idle ×${count}`)
|
|
17
|
+
} else {
|
|
18
|
+
lines.push('● active')
|
|
19
|
+
i++
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n')
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ThinkingMode = 'off' | 'minimal' | 'full'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render the live reasoning/thinking indicator for the given display mode.
|
|
5
|
+
*
|
|
6
|
+
* - `off` → nothing (hide thinking entirely)
|
|
7
|
+
* - `minimal` → a content-free "💭 <label>…" so long reasoning passes don't
|
|
8
|
+
* look like a stall, without dumping the model's chain-of-thought
|
|
9
|
+
* - `full` → the last 200 chars of the actual thinking text (legacy behavior)
|
|
10
|
+
*
|
|
11
|
+
* Returns null when there is no thinking text to show (or mode is `off`).
|
|
12
|
+
*/
|
|
13
|
+
export function formatThinking(
|
|
14
|
+
mode: ThinkingMode,
|
|
15
|
+
thinkingText: string,
|
|
16
|
+
thinkingLabel: string,
|
|
17
|
+
): string | null {
|
|
18
|
+
if (mode === 'off' || !thinkingText) return null
|
|
19
|
+
if (mode === 'minimal') return `💭 ${thinkingLabel}…`
|
|
20
|
+
return `💭 ${thinkingText.slice(-200)}`
|
|
21
|
+
}
|