@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.
@@ -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 = filter.startsWith('/') ? filter.slice(1).toLowerCase() : filter.toLowerCase()
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
- const selected = filtered[cursorIdx]
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 = filter.startsWith('/') ? filter.slice(1) : filter
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
+ }
@@ -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 (_ctx, args) => {
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
- return result.success
2817
- ? { content: result.content }
2818
- : { content: `Loop failed: ${result.error || 'unknown error'}` }
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.startsWith('/') && !prevValueRef.current.startsWith('/')) {
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
+ }