@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/package.json +2 -3
- package/src/agent/message-bus.ts +7 -1
- package/src/agent/sub-agent.ts +7 -1
- package/src/commands/environment.ts +270 -0
- package/src/commands/git.ts +152 -0
- package/src/commands/project.ts +1011 -0
- package/src/core/engine.ts +8 -7
- package/src/core/permission.ts +5 -0
- package/src/core/session-store.ts +4 -1
- package/src/index.tsx +6 -5
- package/src/skills/community-registry.json +72 -0
- package/src/skills/registry.ts +2 -75
- package/src/tools/exec/bash.ts +3 -3
- package/src/tools/exec/enter-worktree.ts +23 -1
- package/src/tools/exec/exit-worktree.ts +13 -0
- package/src/ui/agent-footer.tsx +89 -0
- package/src/ui/app.tsx +217 -96
- package/src/ui/chat.tsx +80 -30
- package/src/ui/commands.ts +202 -1484
- package/src/ui/input.tsx +61 -2
- package/src/ui/strip-indent.ts +19 -0
- package/src/workflow/runtime.ts +11 -1
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({
|
|
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
|
|
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
|
+
}
|
package/src/workflow/runtime.ts
CHANGED
|
@@ -157,7 +157,17 @@ export async function runWorkflow(
|
|
|
157
157
|
sandboxCtx.phase = wrappedPhase
|
|
158
158
|
sandboxCtx.log = log
|
|
159
159
|
|
|
160
|
-
|
|
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)
|