@miphamai/cli 0.33.1 → 0.34.0

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/app.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
3
  import TextInput from 'ink-text-input'
4
+ import { ErrorBoundary } from './error-boundary'
4
5
  import type { QueryEngine } from '../core/engine'
5
6
  import type { RemoteEngine } from '../daemon/remote-engine'
6
7
  import type { MiphamConfig } from '../shared/index.ts'
@@ -410,7 +411,17 @@ export function App({
410
411
  role: msg.role,
411
412
  content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
412
413
  }))
413
- setMessages((prev) => [...prev, ...restored])
414
+ if (result.resumeWarning) {
415
+ // Fix 7: Session isolation — add a visual separator between current session
416
+ // and restored history to prevent accidental leakage/confusion
417
+ const separator: ChatMessage = {
418
+ role: 'assistant' as const,
419
+ content: '── Restored session history below ──',
420
+ }
421
+ setMessages((prev) => [...prev, separator, ...restored])
422
+ } else {
423
+ setMessages((prev) => [...prev, ...restored])
424
+ }
414
425
  }
415
426
  if (result.copyContent) {
416
427
  // Copy to clipboard via pbcopy (macOS) or clip (Windows)
@@ -438,6 +449,24 @@ export function App({
438
449
  }
439
450
  }
440
451
 
452
+ // ── Emotion detection: adjust behavior based on user's emotional state ──
453
+ // Uses regex heuristics (zero-latency) to detect frustration/impatience/confusion.
454
+ // When frustrated, prepends a terseness instruction to the user input so the
455
+ // AI model skips explanations and gets straight to the fix.
456
+ let emotionPrefix = ''
457
+ try {
458
+ const { EmotionDetector } = await import('../core/emotion-detector.js')
459
+ const detector = new EmotionDetector()
460
+ const result = detector.detect(input)
461
+ if (result.emotion === 'frustrated' || result.emotion === 'impatient') {
462
+ emotionPrefix = `[SYSTEM NOTE: The user is ${result.emotion}. Be extremely concise. Skip all explanations, preambles, and summaries. Output only the fix/result. No "here's what I did" or "let me explain". One sentence maximum before code.]\n\n`
463
+ } else if (result.emotion === 'confused') {
464
+ emotionPrefix = `[SYSTEM NOTE: The user seems confused. Explain more thoroughly, break down complex steps, and offer clarifying questions rather than assuming understanding.]\n\n`
465
+ }
466
+ } catch {
467
+ // Emotion detection is non-critical — fail silently
468
+ }
469
+
441
470
  // ── Normal message processing (AI chat) ──
442
471
  setMessages((prev) => [...prev, { role: 'user', content: input }])
443
472
  setIsLoading(true)
@@ -482,7 +511,10 @@ export function App({
482
511
  }
483
512
 
484
513
  try {
485
- for await (const chunk of engine.process(input, controller.signal)) {
514
+ for await (const chunk of engine.process(
515
+ emotionPrefix ? emotionPrefix + input : input,
516
+ controller.signal,
517
+ )) {
486
518
  // Reasoning content (DeepSeek V4 thinking mode) — silently consumed,
487
519
  // not shown to user to avoid noise.
488
520
  if (chunk.reasoning_content) {
@@ -743,140 +775,142 @@ export function App({
743
775
  }
744
776
 
745
777
  return (
746
- <Box flexDirection="column" padding={1} height="100%">
747
- {/* Workflow progress — auto-detects active workflows, renders nothing when idle */}
748
- <WorkflowProgress />
749
-
750
- {/* Agent View Dashboard — Ctrl+G overlay (replaces chat + input) */}
751
- {agentViewOpen && agentViewManager ? (
752
- <AgentViewDashboard
753
- manager={agentViewManager}
754
- onAttach={() => {}}
755
- onExit={() => setAgentViewOpen(false)}
756
- />
757
- ) : (
758
- <>
759
- {/* Chat panel */}
760
- <ChatPanel messages={messages} focusMode={focusMode} />
761
-
762
- {/* Input with separator lines */}
763
- {pickerOpen ? (
764
- <ModelPicker
765
- config={config}
766
- currentProvider={providerId}
767
- currentModel={modelId}
768
- onSelect={(newProvider, newModel) => {
769
- engine.switchProvider(newProvider, newModel)
770
- setProviderId(newProvider)
771
- setModelId(newModel)
772
- setPickerOpen(false)
773
- setMessages((prev) => [
774
- ...prev,
775
- { role: 'system', content: `✓ Switched to ${newProvider}/${newModel}` },
776
- ])
777
- }}
778
- onNeedsApiKey={(providerId, modelId, providerName) => {
779
- setApiKeyPrompt({ providerId, modelId, providerName })
780
- setApiKeyInput('')
781
- setPickerOpen(false)
782
- setMessages((prev) => [
783
- ...prev,
784
- {
785
- role: 'system',
786
- content: t('ui.picker.needs_api_key', { provider: providerName }),
787
- },
788
- ])
789
- }}
790
- onClose={() => setPickerOpen(false)}
791
- />
792
- ) : (
793
- /* Input bar (hidden when picker is open) */
794
- <Box flexDirection="column">
795
- <Text dimColor>──────────────────────────────</Text>
796
- <InputBar
797
- onSubmit={handleSubmit}
798
- isLoading={isLoading}
799
- onTogglePicker={() => setPickerOpen((prev) => !prev)}
800
- onToggleFocus={() => setFocusMode((prev) => !prev)}
801
- onToggleExpand={() => {
802
- setMessages((prev) => {
803
- const msgs = [...prev]
804
- for (let i = msgs.length - 1; i >= 0; i--) {
805
- if (msgs[i]?.toolMeta) {
806
- const meta = msgs[i]!.toolMeta!
807
- if (meta.collapsed) {
808
- msgs[i] = {
809
- ...msgs[i]!,
810
- content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
811
- toolMeta: { ...meta, collapsed: false },
812
- }
813
- } else {
814
- const short =
815
- meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
816
- msgs[i] = {
817
- ...msgs[i]!,
818
- content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
819
- toolMeta: { ...meta, collapsed: true },
778
+ <ErrorBoundary>
779
+ <Box flexDirection="column" padding={1} height="100%">
780
+ {/* Workflow progress — auto-detects active workflows, renders nothing when idle */}
781
+ <WorkflowProgress />
782
+
783
+ {/* Agent View Dashboard — Ctrl+G overlay (replaces chat + input) */}
784
+ {agentViewOpen && agentViewManager ? (
785
+ <AgentViewDashboard
786
+ manager={agentViewManager}
787
+ onAttach={() => {}}
788
+ onExit={() => setAgentViewOpen(false)}
789
+ />
790
+ ) : (
791
+ <>
792
+ {/* Chat panel */}
793
+ <ChatPanel messages={messages} focusMode={focusMode} />
794
+
795
+ {/* Input with separator lines */}
796
+ {pickerOpen ? (
797
+ <ModelPicker
798
+ config={config}
799
+ currentProvider={providerId}
800
+ currentModel={modelId}
801
+ onSelect={(newProvider, newModel) => {
802
+ engine.switchProvider(newProvider, newModel)
803
+ setProviderId(newProvider)
804
+ setModelId(newModel)
805
+ setPickerOpen(false)
806
+ setMessages((prev) => [
807
+ ...prev,
808
+ { role: 'system', content: `✓ Switched to ${newProvider}/${newModel}` },
809
+ ])
810
+ }}
811
+ onNeedsApiKey={(providerId, modelId, providerName) => {
812
+ setApiKeyPrompt({ providerId, modelId, providerName })
813
+ setApiKeyInput('')
814
+ setPickerOpen(false)
815
+ setMessages((prev) => [
816
+ ...prev,
817
+ {
818
+ role: 'system',
819
+ content: t('ui.picker.needs_api_key', { provider: providerName }),
820
+ },
821
+ ])
822
+ }}
823
+ onClose={() => setPickerOpen(false)}
824
+ />
825
+ ) : (
826
+ /* Input bar (hidden when picker is open) */
827
+ <Box flexDirection="column">
828
+ <Text dimColor>──────────────────────────────</Text>
829
+ <InputBar
830
+ onSubmit={handleSubmit}
831
+ isLoading={isLoading}
832
+ onTogglePicker={() => setPickerOpen((prev) => !prev)}
833
+ onToggleFocus={() => setFocusMode((prev) => !prev)}
834
+ onToggleExpand={() => {
835
+ setMessages((prev) => {
836
+ const msgs = [...prev]
837
+ for (let i = msgs.length - 1; i >= 0; i--) {
838
+ if (msgs[i]?.toolMeta) {
839
+ const meta = msgs[i]!.toolMeta!
840
+ if (meta.collapsed) {
841
+ msgs[i] = {
842
+ ...msgs[i]!,
843
+ content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
844
+ toolMeta: { ...meta, collapsed: false },
845
+ }
846
+ } else {
847
+ const short =
848
+ meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
849
+ msgs[i] = {
850
+ ...msgs[i]!,
851
+ content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
852
+ toolMeta: { ...meta, collapsed: true },
853
+ }
820
854
  }
855
+ break
821
856
  }
822
- break
823
857
  }
858
+ return msgs
859
+ })
860
+ }}
861
+ onCyclePermission={() => {
862
+ setPermissionMode((prev) => {
863
+ const idx = PERMISSION_MODES.indexOf(prev)
864
+ const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
865
+ engine.getPermission().setMode(next)
866
+ return next
867
+ })
868
+ }}
869
+ onCancel={() => {
870
+ if (abortRef.current) {
871
+ abortRef.current.abort()
824
872
  }
825
- return msgs
826
- })
827
- }}
828
- onCyclePermission={() => {
829
- setPermissionMode((prev) => {
830
- const idx = PERMISSION_MODES.indexOf(prev)
831
- const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
832
- engine.getPermission().setMode(next)
833
- return next
834
- })
835
- }}
836
- onCancel={() => {
837
- if (abortRef.current) {
838
- abortRef.current.abort()
839
- }
840
- }}
841
- onToggleAgentView={() => setAgentViewOpen((prev) => !prev)}
842
- />
843
- <Text dimColor>──────────────────────────────</Text>
844
- </Box>
845
- )}
846
-
847
- {/* Agent status footer — shows active tool indicator + inline agent progress + running background agents */}
848
- <AgentFooter
849
- agents={Object.values(runningAgents)}
850
- tick={agentTick}
851
- activeTool={activeTool}
852
- agentProgress={agentProgress}
853
- />
854
-
855
- {/* Status line — Claude Code style */}
856
- <Box marginTop={1} flexDirection="column">
857
- {goalText && (
858
- <Box>
859
- <Text color="green">🎯 Goal: {goalText}</Text>
873
+ }}
874
+ onToggleAgentView={() => setAgentViewOpen((prev) => !prev)}
875
+ />
876
+ <Text dimColor>──────────────────────────────</Text>
860
877
  </Box>
861
878
  )}
862
- <Box flexDirection="row">
863
- <Text color={PERMISSION_COLORS[permissionMode]}>
864
- ⏵⏵ {PERMISSION_LABELS[permissionMode]}
865
- </Text>
866
- <Text dimColor>
867
- {' '}
868
- ({t('ui.status.shift_tab_cycle')}: {PERMISSION_LABELS.default} ·{' '}
869
- {PERMISSION_LABELS.acceptEdits} · {PERMISSION_LABELS.bypassPermissions} ·{' '}
870
- {PERMISSION_LABELS.plan} · {PERMISSION_LABELS.auto} · {PERMISSION_LABELS.dontAsk})
871
- {' · '}
872
- {t('ui.status.esc_to_interrupt')}
873
- {' · '}
874
- {t('ui.status.left_for_agents')}
875
- </Text>
879
+
880
+ {/* Agent status footer — shows active tool indicator + inline agent progress + running background agents */}
881
+ <AgentFooter
882
+ agents={Object.values(runningAgents)}
883
+ tick={agentTick}
884
+ activeTool={activeTool}
885
+ agentProgress={agentProgress}
886
+ />
887
+
888
+ {/* Status line — Claude Code style */}
889
+ <Box marginTop={1} flexDirection="column">
890
+ {goalText && (
891
+ <Box>
892
+ <Text color="green">🎯 Goal: {goalText}</Text>
893
+ </Box>
894
+ )}
895
+ <Box flexDirection="row">
896
+ <Text color={PERMISSION_COLORS[permissionMode]}>
897
+ ⏵⏵ {PERMISSION_LABELS[permissionMode]}
898
+ </Text>
899
+ <Text dimColor>
900
+ {' '}
901
+ ({t('ui.status.shift_tab_cycle')}: {PERMISSION_LABELS.default} ·{' '}
902
+ {PERMISSION_LABELS.acceptEdits} · {PERMISSION_LABELS.bypassPermissions} ·{' '}
903
+ {PERMISSION_LABELS.plan} · {PERMISSION_LABELS.auto} · {PERMISSION_LABELS.dontAsk})
904
+ {' · '}
905
+ {t('ui.status.esc_to_interrupt')}
906
+ {' · '}
907
+ {t('ui.status.left_for_agents')}
908
+ </Text>
909
+ </Box>
876
910
  </Box>
877
- </Box>
878
- </>
879
- )}
880
- </Box>
911
+ </>
912
+ )}
913
+ </Box>
914
+ </ErrorBoundary>
881
915
  )
882
916
  }