@miphamai/cli 0.60.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.60.0",
3
+ "version": "0.60.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -13,7 +13,9 @@ export const DEFAULT_CONFIG: MiphamConfig = {
13
13
  defaultProvider: 'deepseek',
14
14
  defaultModel: 'deepseek-v4-pro',
15
15
  permission: 'default',
16
- showThinking: 'minimal',
16
+ showThinking: 'off',
17
+ showSchedulingNotices: false,
18
+ showCommandPicker: false,
17
19
  // Org 级权限限制(可选):forbiddenModes 禁指定模式 / maxAllowedMode 封顶层级;
18
20
  // 请求被禁模式时 fail-closed 降级(如 forbiddenModes:['bypassPermissions'])。
19
21
  // permissionRestrictions: { forbiddenModes: ['bypassPermissions'] },
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.60.0' as const
12
+ export const PACKAGE_VERSION = '0.60.1' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -137,11 +137,15 @@ export interface MiphamConfig {
137
137
  permissionRules?: { allow?: string[]; deny?: string[] }
138
138
  /**
139
139
  * How to render the model's reasoning/thinking before the answer:
140
- * - `off` → hide entirely
141
- * - `minimal` → content-free "thinking…" indicator (default)
140
+ * - `off` → hide entirely (default — clean output)
141
+ * - `minimal` → content-free "thinking…" indicator
142
142
  * - `full` → last 200 chars of the actual thinking text
143
143
  */
144
144
  showThinking?: 'off' | 'minimal' | 'full'
145
+ /** When false, hide the `⏰ Wakeup scheduled` scheduling confirmation notices. Default false. */
146
+ showSchedulingNotices?: boolean
147
+ /** When false, disable the slash-command picker auto-popup on `/`. Default false. */
148
+ showCommandPicker?: boolean
145
149
  providers: ProviderConfig[]
146
150
  skills?: { paths: string[]; mcpServers: McpServerConfig[] }
147
151
  marketplace?: {
package/src/ui/app.tsx CHANGED
@@ -446,10 +446,17 @@ export function App({
446
446
  clearTimeout(thinkingTimerRef.current)
447
447
  thinkingTimerRef.current = null
448
448
  }
449
- setMessages((prev) => [
450
- ...prev,
451
- { role: 'system' as const, content: `💭 ${thought}` },
452
- ])
449
+ const thinkingLine = formatThinking(
450
+ config.showThinking ?? 'off',
451
+ thought,
452
+ t('ui.loading.thinking'),
453
+ )
454
+ if (thinkingLine) {
455
+ setMessages((prev) => [
456
+ ...prev,
457
+ { role: 'system' as const, content: thinkingLine },
458
+ ])
459
+ }
453
460
  }
454
461
  turnContent = chunk.content
455
462
  isNewTurn = false
@@ -668,7 +675,7 @@ export function App({
668
675
  // Turn finished — drain any /loop wakeup queued while we were running.
669
676
  drainLoopQueueRef.current?.(turnId)
670
677
  },
671
- [engine, syncBgAgents],
678
+ [engine, syncBgAgents, config, t],
672
679
  )
673
680
 
674
681
  // Ref-held drain breaks the runTurn ↔ drain circular useCallback dependency: runTurn
@@ -811,11 +818,11 @@ export function App({
811
818
  setApiKeyPrompt(result.needsApiKey)
812
819
  setApiKeyInput('')
813
820
  }
814
- setMessages((prev) => [
815
- ...prev,
816
- { role: 'user', content: input },
817
- { role: 'system', content: result.content },
818
- ])
821
+ setMessages((prev) => {
822
+ const next: ChatMessage[] = [...prev, { role: 'user', content: input }]
823
+ if (result.content) next.push({ role: 'system', content: result.content })
824
+ return next
825
+ })
819
826
  if (result.needsApiKey) {
820
827
  // Don't process nextProvider/model when waiting for API key
821
828
  if (result.exit) process.exit(0)
@@ -1019,7 +1026,7 @@ export function App({
1019
1026
  <ChatPanel messages={messages} focusMode={focusMode} />
1020
1027
  {(() => {
1021
1028
  const indicator = formatThinking(
1022
- config.showThinking ?? 'minimal',
1029
+ config.showThinking ?? 'off',
1023
1030
  thinkingText,
1024
1031
  t('ui.loading.thinking'),
1025
1032
  )
@@ -1063,6 +1070,7 @@ export function App({
1063
1070
  <InputBar
1064
1071
  onSubmit={handleSubmit}
1065
1072
  isLoading={isLoading}
1073
+ showCommandPicker={config.showCommandPicker ?? false}
1066
1074
  onTogglePicker={() => setPickerOpen((prev) => !prev)}
1067
1075
  onToggleFocus={() => setFocusMode((prev) => !prev)}
1068
1076
  onToggleExpand={() => {
@@ -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
+ }
@@ -546,7 +546,9 @@ const configCmd: CommandHandler = (ctx) => {
546
546
  `defaultProvider: ${c.defaultProvider}`,
547
547
  `defaultModel: ${c.defaultModel}`,
548
548
  `permission: ${c.permission}`,
549
- `showThinking: ${c.showThinking ?? 'minimal'}`,
549
+ `showThinking: ${c.showThinking ?? 'off'}`,
550
+ `showSchedulingNotices: ${c.showSchedulingNotices ?? false}`,
551
+ `showCommandPicker: ${c.showCommandPicker ?? false}`,
550
552
  `providers: ${c.providers.length} configured`,
551
553
  '',
552
554
  `crossSessionInbound: ${xs.crossSessionInbound}`,
@@ -2821,9 +2823,14 @@ const loopCmd: CommandHandler = async (ctx, args) => {
2821
2823
  { delaySeconds: seconds, reason: `Loop: ${prompt.slice(0, 40)}`, prompt },
2822
2824
  { cwd: process.cwd(), sessionId: 'loop-session', provider: '', model: '' },
2823
2825
  )
2824
- return result.success
2825
- ? { content: result.content }
2826
- : { 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 }
2827
2834
  } catch (e) {
2828
2835
  return { content: `Loop failed: ${e instanceof Error ? e.message : String(e)}` }
2829
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(() => {