@miphamai/cli 0.31.1 → 0.32.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/src/index.tsx CHANGED
@@ -47,6 +47,13 @@ import enUS from './i18n-core/locales/en-US.json' with { type: 'json' }
47
47
  import zhCN from './i18n-core/locales/zh-CN.json' with { type: 'json' }
48
48
  import type { TranslationMap } from './i18n-core/types'
49
49
 
50
+ // Locale bundles — defined at module level so the remote-attach branch can
51
+ // reference them before the full bootstrap path runs.
52
+ const localeBundles: Record<string, TranslationMap> = {
53
+ 'en-US': enUS as TranslationMap,
54
+ 'zh-CN': zhCN as TranslationMap,
55
+ }
56
+
50
57
  interface RunOptions {
51
58
  model?: string
52
59
  provider?: string
@@ -54,6 +61,8 @@ interface RunOptions {
54
61
  permission?: string
55
62
  resume?: string
56
63
  version?: string
64
+ /** When set, launch TUI in remote mode connected to a daemon session. */
65
+ remoteSession?: { sessionId: string; port: number; token: string }
57
66
  }
58
67
 
59
68
  /**
@@ -183,6 +192,41 @@ export async function runApp(options: RunOptions): Promise<void> {
183
192
  // ── Workspace Trust Check ──
184
193
  await checkWorkspaceTrust()
185
194
 
195
+ // ── Remote attach mode ──────────────────────────────────────────────────
196
+ // When the TUI is launched via `mipham attach`, skip the entire local
197
+ // engine bootstrap and connect to the daemon via WebSocket instead.
198
+ if (options.remoteSession) {
199
+ const { RemoteEngine } = await import('./daemon/remote-engine')
200
+ const engine = new RemoteEngine(options.remoteSession)
201
+
202
+ // Detect locale and create translation function (same as local path)
203
+ const locale = detectLocale({ lang: options.lang })
204
+ const t = createT(localeBundles[locale] || enUS, enUS)
205
+
206
+ // Set terminal window title
207
+ process.stdout.write('\x1b]0;Mipham Code\x07')
208
+
209
+ const { waitUntilExit } = render(
210
+ React.createElement(I18nProvider, {
211
+ locale,
212
+ t,
213
+ children: React.createElement(App, {
214
+ engine,
215
+ config: {} as MiphamConfig,
216
+ initialProvider: 'remote',
217
+ initialModel: 'remote',
218
+ version: options.version,
219
+ sessionId: options.remoteSession.sessionId,
220
+ }),
221
+ }),
222
+ )
223
+ await waitUntilExit()
224
+
225
+ // Close the WebSocket connection on the way out
226
+ engine.close()
227
+ return
228
+ }
229
+
186
230
  // Handle `mipham agents` subcommand — launch standalone dashboard
187
231
  const args = process.argv.slice(2)
188
232
  if (args[0] === 'agents') {
@@ -208,10 +252,6 @@ export async function runApp(options: RunOptions): Promise<void> {
208
252
 
209
253
  // Detect locale and create translation function
210
254
  const locale = detectLocale({ lang: options.lang })
211
- const localeBundles: Record<string, TranslationMap> = {
212
- 'en-US': enUS as TranslationMap,
213
- 'zh-CN': zhCN as TranslationMap,
214
- }
215
255
  const t = createT(localeBundles[locale] || enUS, enUS)
216
256
 
217
257
  // Bootstrap providers
package/src/ui/app.tsx CHANGED
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from 'ink'
3
3
  import TextInput from 'ink-text-input'
4
4
  import { readFileSync } from 'node:fs'
5
5
  import type { QueryEngine } from '../core/engine'
6
+ import type { RemoteEngine } from '../daemon/remote-engine'
6
7
  import type { MiphamConfig } from '../shared/index.ts'
7
8
  import type { SkillsLoader } from '../skills/loader'
8
9
  import type { PluginManager } from '../plugin/plugin-manager'
@@ -29,7 +30,7 @@ import type { PermissionMode } from '../shared/index.ts'
29
30
  import { sanitizeForDisplay } from '../shared/sanitize.ts'
30
31
 
31
32
  interface AppProps {
32
- engine: QueryEngine
33
+ engine: QueryEngine | RemoteEngine
33
34
  config: MiphamConfig
34
35
  initialProvider?: string
35
36
  initialModel?: string
@@ -265,7 +266,10 @@ export function App({
265
266
 
266
267
  const mkCtx = useCallback(
267
268
  (): CommandContext => ({
268
- engine,
269
+ // In remote attach mode, RemoteEngine provides stub methods for all the
270
+ // QueryEngine APIs that slash commands need. The cast is safe because
271
+ // stubs cover every method that CommandContext consumers call.
272
+ engine: engine as QueryEngine,
269
273
  config,
270
274
  providerId,
271
275
  modelId,
package/src/ui/chat.tsx CHANGED
@@ -157,52 +157,58 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
157
157
  }
158
158
 
159
159
  /**
160
- * Group consecutive tool calls into compact one-liners for the normal view.
161
- * Single tools show normally; 2+ consecutive tools get folded:
162
- * ⏺ Read · Glob · Bash (3 tools)
160
+ * Pair each tool_use message with its following tool_result message(s),
161
+ * showing every tool individually with its name, detail, and output.
162
+ *
163
+ * Claude Code parity: each tool renders as:
164
+ * ⏺ ToolName (detail) ← colored per tool type
165
+ * ⎿ output summary ← dimmed below
166
+ *
167
+ * Only true duplicates (same tool name + same detail) are collapsed
168
+ * into a single entry with a count.
163
169
  */
164
170
  function compactToolGroups(messages: ChatMessage[]): ChatMessage[] {
165
171
  const result: ChatMessage[] = []
166
- let toolGroup: ChatMessage[] = []
172
+ let current: ChatMessage | null = null
167
173
 
168
- const flush = () => {
169
- if (toolGroup.length === 0) return
170
- if (toolGroup.length === 1) {
171
- result.push(toolGroup[0]!)
172
- } else {
173
- const names = toolGroup
174
- .filter((t) => t.toolMeta?.name && !t.toolMeta.name.startsWith('tools ('))
175
- .map((t) => t.toolMeta!.name)
176
- const uniqueNames = [...new Set(names)]
177
- result.push({
178
- role: 'system',
179
- content: `⏺ ${uniqueNames.join(' · ')} (${names.length} tools)`,
180
- toolMeta: {
181
- name: `tools (${names.length})`,
182
- input: uniqueNames.join(', '),
183
- collapsed: true,
184
- },
185
- })
174
+ for (const msg of messages) {
175
+ // Tool call with a name → flush previous, start a new one
176
+ if (msg.toolMeta?.name && !msg.toolMeta.name.startsWith('tools (')) {
177
+ if (current) result.push(current)
178
+ current = { ...msg }
179
+ continue
186
180
  }
187
- toolGroup = []
188
- }
189
181
 
190
- for (const msg of messages) {
191
- // Tool results (no name, has output) get folded into the preceding tool group
182
+ // Tool result (no name, has output) → merge output into current tool
192
183
  if (msg.toolMeta && !msg.toolMeta.name && msg.toolMeta.output) {
193
- toolGroup.push(msg)
184
+ if (current) {
185
+ current = {
186
+ ...current,
187
+ content: msg.content,
188
+ toolMeta: {
189
+ ...current.toolMeta!,
190
+ output: msg.toolMeta.output,
191
+ },
192
+ }
193
+ }
194
+ // Orphan tool result (no preceding tool_use) → skip
194
195
  continue
195
196
  }
196
- // Tool calls with a name
197
- if (msg.toolMeta?.name && !msg.toolMeta.name.startsWith('tools (')) {
198
- toolGroup.push(msg)
197
+
198
+ // Already-grouped message (from a previous compact cycle) or non-tool
199
+ if (current) {
200
+ result.push(current)
201
+ current = null
202
+ }
203
+ // Skip orphan tool-result messages without a preceding tool_use
204
+ if (msg.toolMeta && !msg.toolMeta.name && msg.toolMeta.output) {
199
205
  continue
200
206
  }
201
- // Already-grouped or non-tool message
202
- flush()
203
207
  result.push(msg)
204
208
  }
205
- flush()
209
+
210
+ // Flush any remaining tool
211
+ if (current) result.push(current)
206
212
  return result
207
213
  }
208
214