@miphamai/cli 0.16.0 → 0.16.2

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/ui/input.tsx CHANGED
@@ -8,6 +8,16 @@ import { CommandPicker } from './command-picker.js'
8
8
  interface InputBarProps {
9
9
  onSubmit: (input: string) => void
10
10
  isLoading: boolean
11
+ /** Ctrl+P → open model picker */
12
+ onTogglePicker?: () => void
13
+ /** Ctrl+F → toggle focus mode */
14
+ onToggleFocus?: () => void
15
+ /** Ctrl+O → expand last tool call */
16
+ onToggleExpand?: () => void
17
+ /** Shift+Tab → cycle permission mode */
18
+ onCyclePermission?: () => void
19
+ /** Escape → cancel loading (when input is empty) */
20
+ onCancel?: () => void
11
21
  }
12
22
 
13
23
  // ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
@@ -107,7 +117,15 @@ function pick<T>(arr: T[]): T {
107
117
  return arr[Math.floor(Math.random() * arr.length)]!
108
118
  }
109
119
 
110
- export function InputBar({ onSubmit, isLoading }: InputBarProps) {
120
+ export function InputBar({
121
+ onSubmit,
122
+ isLoading,
123
+ onTogglePicker,
124
+ onToggleFocus,
125
+ onToggleExpand,
126
+ onCyclePermission,
127
+ onCancel,
128
+ }: InputBarProps) {
111
129
  const [value, setValue] = useState('')
112
130
  const [verb, setVerb] = useState(() => pick(STATUS_GERUNDS))
113
131
  const [completionVerb, setCompletionVerb] = useState<string | null>(null)
@@ -158,10 +176,19 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
158
176
  const [searchMode, setSearchMode] = useState(false)
159
177
  const [searchQuery, setSearchQuery] = useState('')
160
178
 
179
+ // Track pre-shortcut value so we can revert ink-text-input's Ctrl-key insertions.
180
+ // ink-text-input inserts 'p'/'f'/'o' for Ctrl+P/F/O (Ink normalizes control chars
181
+ // to their letter names before passing to useInput).
182
+ const valueBeforeShortcut = useRef(value)
183
+ // Keep the ref in sync with state on every change that isn't a Ctrl shortcut revert.
184
+ useEffect(() => {
185
+ valueBeforeShortcut.current = value
186
+ }, [value])
187
+
161
188
  // ── Vim motions: intercept keys in normal mode ──
162
189
 
163
190
  useInput((input, key) => {
164
- // Escape toggles between insert and normal mode
191
+ // ── Escape: cancel loading → toggle vim mode ──
165
192
  if (key.escape) {
166
193
  // Cancel search mode if active
167
194
  if (searchMode) {
@@ -169,6 +196,11 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
169
196
  setSearchQuery('')
170
197
  return
171
198
  }
199
+ // Escape while loading → abort (priority over vim toggle)
200
+ if (isLoading) {
201
+ onCancel?.()
202
+ return
203
+ }
172
204
  // Clear any pending multi-key sequence
173
205
  if (vimPending.current) {
174
206
  vimPending.current = null
@@ -178,6 +210,33 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
178
210
  return
179
211
  }
180
212
 
213
+ // ── Global hotkeys (work in both insert and normal modes) ──
214
+ // Shift+Tab → cycle permission mode
215
+ if (key.shift && key.tab) {
216
+ onCyclePermission?.()
217
+ return
218
+ }
219
+ // Ctrl+P → toggle model picker
220
+ // NOTE: Ink passes input=keypress.name (just 'p') when ctrl is true, not raw \x10.
221
+ // ink-text-input inserts 'p' as literal text — revert it.
222
+ if (key.ctrl && input === 'p') {
223
+ setValue(valueBeforeShortcut.current)
224
+ onTogglePicker?.()
225
+ return
226
+ }
227
+ // Ctrl+F → toggle focus mode
228
+ if (key.ctrl && input === 'f') {
229
+ setValue(valueBeforeShortcut.current)
230
+ onToggleFocus?.()
231
+ return
232
+ }
233
+ // Ctrl+O → expand/collapse last tool call
234
+ if (key.ctrl && input === 'o') {
235
+ setValue(valueBeforeShortcut.current)
236
+ onToggleExpand?.()
237
+ return
238
+ }
239
+
181
240
  if (vimEngine.current.mode !== 'normal') return
182
241
 
183
242
  // Handle search mode — collect query characters
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Template literal tag that strips leading indentation from multi-line strings.
3
+ * Used by slash command handlers to format help text and status messages.
4
+ */
5
+ export function stripIndent(strings: TemplateStringsArray, ...values: unknown[]): string {
6
+ let result = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '')
7
+ // Remove leading newline
8
+ result = result.replace(/^\n/, '')
9
+ // Find minimum indent
10
+ const match = result.match(/^( +)/m)
11
+ if (match?.[1]) {
12
+ const indent = match[1].length
13
+ result = result
14
+ .split('\n')
15
+ .map((line) => line.slice(indent))
16
+ .join('\n')
17
+ }
18
+ return result.trim()
19
+ }
@@ -157,7 +157,17 @@ export async function runWorkflow(
157
157
  sandboxCtx.phase = wrappedPhase
158
158
  sandboxCtx.log = log
159
159
 
160
- const result = await vmScript.runInContext(sandboxCtx, { timeout: 120_000 })
160
+ let result: unknown
161
+ try {
162
+ result = await vmScript.runInContext(sandboxCtx, { timeout: 120_000 })
163
+ } catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err)
165
+ throw new Error(
166
+ `Workflow script execution failed: ${message}\n\n` +
167
+ `This is likely a bug in the workflow script, not in Mipham Code itself. ` +
168
+ `Check the script for syntax errors, undefined references, or unhandled promise rejections.`,
169
+ )
170
+ }
161
171
 
162
172
  // Count journal entries from state
163
173
  const priorEntries = loadJournal(runId)