mixdog 0.9.86 → 0.9.87
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 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/session-runtime/lifecycle-api.mjs +15 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/runtime-core.mjs +267 -481
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/standalone/agent-shard/shard-pool.mjs +6 -5
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +100 -1151
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2576
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/dist/index.mjs +9802 -9024
- package/src/tui/engine/session-api-ext.mjs +23 -0
package/src/tui/App.jsx
CHANGED
|
@@ -33,17 +33,13 @@ import { UsagePanel } from './components/UsagePanel.jsx';
|
|
|
33
33
|
import { TextEntryPanel } from './components/TextEntryPanel.jsx';
|
|
34
34
|
import {
|
|
35
35
|
buildPromptContentWithImages,
|
|
36
|
-
|
|
36
|
+
expandPastedTextTokens,
|
|
37
37
|
imageReferenceIds,
|
|
38
|
+
pastedTextReferenceIds,
|
|
38
39
|
readClipboardImageAttachment,
|
|
39
40
|
readImageAttachmentFromPath,
|
|
40
|
-
splitPastedImagePathCandidates,
|
|
41
|
-
} from './paste-attachments.mjs';
|
|
42
|
-
import {
|
|
43
|
-
expandPastedTextTokens,
|
|
44
|
-
formatPastedTextRef,
|
|
45
|
-
pastedTextReferenceIds,
|
|
46
41
|
shouldFoldPastedText,
|
|
42
|
+
splitPastedImagePathCandidates,
|
|
47
43
|
} from './paste-attachments.mjs';
|
|
48
44
|
import { formatDuration } from './time-format.mjs';
|
|
49
45
|
import {
|
|
@@ -91,6 +87,20 @@ import { wrappedTextRows, promptContentRows, wrappedDetailRows, textEntryReserve
|
|
|
91
87
|
import stringWidth from 'string-width';
|
|
92
88
|
import { useMouseInput } from './app/use-mouse-input.mjs';
|
|
93
89
|
import { useTranscriptScroll } from './app/use-transcript-scroll.mjs';
|
|
90
|
+
import { usePastedBuffers } from './app/use-pasted-buffers.mjs';
|
|
91
|
+
import { usePromptHint } from './app/use-prompt-hint.mjs';
|
|
92
|
+
import { useWelcomePromptHint } from './app/use-welcome-prompt-hint.mjs';
|
|
93
|
+
import { useCopySelection } from './app/use-copy-selection.mjs';
|
|
94
|
+
import { createUsageContextPanels } from './app/usage-context-panels.mjs';
|
|
95
|
+
import { createPromptSubmit } from './app/prompt-submit.mjs';
|
|
96
|
+
import { computeShellLayout } from './app/shell-layout.mjs';
|
|
97
|
+
import { useGlobalKeyInput } from './app/use-global-key-input.mjs';
|
|
98
|
+
import { renderAppView } from './app/app-view.jsx';
|
|
99
|
+
import { usePromptDraftFlow } from './app/use-prompt-draft-flow.mjs';
|
|
100
|
+
import { useTranscriptActivity } from './app/use-transcript-activity.mjs';
|
|
101
|
+
import { createAppPickers } from './app/create-app-pickers.mjs';
|
|
102
|
+
import { usePromptQueueHistory } from './app/use-prompt-queue-history.mjs';
|
|
103
|
+
import { useTerminalChrome } from './app/use-terminal-chrome.mjs';
|
|
94
104
|
import { useTranscriptWindow } from './app/use-transcript-window.mjs';
|
|
95
105
|
import {
|
|
96
106
|
TRANSCRIPT_WINDOW_MIN_ITEMS,
|
|
@@ -128,10 +138,6 @@ import {
|
|
|
128
138
|
formatSessionMessageCount,
|
|
129
139
|
fitLine,
|
|
130
140
|
centerLine,
|
|
131
|
-
CONDITIONAL_WELCOME_PROMPT_HINTS,
|
|
132
|
-
randomWelcomePromptHint,
|
|
133
|
-
providerSetupHasUsableProvider,
|
|
134
|
-
activeWorkflowSummaryForStore,
|
|
135
141
|
promptStatusColor,
|
|
136
142
|
promptHistoryKey,
|
|
137
143
|
} from './app/app-format.mjs';
|
|
@@ -419,39 +425,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
419
425
|
beginRenameProject,
|
|
420
426
|
openProjectPicker,
|
|
421
427
|
} = projectPicker;
|
|
422
|
-
// Theme/effort picker cluster — same dep set as the project picker; the
|
|
423
|
-
// destructured openers are used inside handlers defined later in the body.
|
|
424
|
-
const { openThemePicker, openEffortPicker } = createThemeEffortPickers({
|
|
425
|
-
state,
|
|
426
|
-
store,
|
|
427
|
-
setPicker,
|
|
428
|
-
setProviderPrompt,
|
|
429
|
-
setChannelPrompt,
|
|
430
|
-
setHookPrompt,
|
|
431
|
-
setSettingsPrompt,
|
|
432
|
-
setContextPanel,
|
|
433
|
-
closeUsagePanel,
|
|
434
|
-
clean,
|
|
435
|
-
});
|
|
436
|
-
// Resume picker — independent (store + setPicker + two session formatters).
|
|
437
|
-
const { openResumePicker } = createResumePicker({
|
|
438
|
-
store,
|
|
439
|
-
setPicker,
|
|
440
|
-
formatSessionUpdatedAt,
|
|
441
|
-
formatSessionMessageCount,
|
|
442
|
-
});
|
|
443
|
-
// Core-memory picker cluster. The Esc-return target is passed per entry
|
|
444
|
-
// point ({ returnTo }): Settings threads openSettingsPicker, standalone
|
|
445
|
-
// /memory passes null so Esc closes instead of surfacing Settings.
|
|
446
|
-
// Only openMemoryCorePicker is called from the App body; the entry-action /
|
|
447
|
-
// add / edit / delete openers are internal to the factory (reached through
|
|
448
|
-
// the picker's own onSelect closures), so they stay unbound here.
|
|
449
|
-
const { openMemoryCorePicker } = createCoreMemoryPicker({
|
|
450
|
-
store,
|
|
451
|
-
setPicker,
|
|
452
|
-
setSettingsPrompt,
|
|
453
|
-
parseMemoryCoreRows,
|
|
454
|
-
});
|
|
455
428
|
const [disabledSkills, setDisabledSkillsInner] = useState(() => {
|
|
456
429
|
try {
|
|
457
430
|
const { disabled = [] } = store.getDisabledSkills?.() || {};
|
|
@@ -472,61 +445,28 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
472
445
|
return set;
|
|
473
446
|
});
|
|
474
447
|
}, [store]);
|
|
475
|
-
const {
|
|
476
|
-
openMcpServersPicker,
|
|
477
|
-
openMcpPicker,
|
|
478
|
-
openProjectSkillsPicker,
|
|
479
|
-
openSkillsPicker,
|
|
480
|
-
openSkillDetailPicker,
|
|
481
|
-
beginAddPlugin,
|
|
482
|
-
openPluginDetailPicker,
|
|
483
|
-
openInstalledPluginsPicker,
|
|
484
|
-
openPluginsPicker,
|
|
485
|
-
openHooksPicker,
|
|
486
|
-
} = createExtensionPickers({
|
|
487
|
-
store,
|
|
488
|
-
theme,
|
|
489
|
-
clean,
|
|
490
|
-
copyToClipboard,
|
|
491
|
-
setPicker,
|
|
492
|
-
getPicker: () => livePickerRef.current,
|
|
493
|
-
setProviderPrompt,
|
|
494
|
-
setChannelPrompt,
|
|
495
|
-
setHookPrompt,
|
|
496
|
-
setSettingsPrompt,
|
|
497
|
-
getDisabledSkills: () => disabledSkills,
|
|
498
|
-
setDisabledSkills,
|
|
499
|
-
});
|
|
500
|
-
const {
|
|
501
|
-
openUpdatePicker,
|
|
502
|
-
openAutoClearPicker,
|
|
503
|
-
openProfilePicker,
|
|
504
|
-
} = createMaintenancePickers({
|
|
505
|
-
store,
|
|
506
|
-
theme,
|
|
507
|
-
formatDuration,
|
|
508
|
-
setPicker,
|
|
509
|
-
setProviderPrompt,
|
|
510
|
-
setChannelPrompt,
|
|
511
|
-
setHookPrompt,
|
|
512
|
-
setSettingsPrompt,
|
|
513
|
-
setContextPanel,
|
|
514
|
-
closeUsagePanel,
|
|
515
|
-
});
|
|
516
448
|
const toolApproval = state.toolApproval || null;
|
|
517
449
|
const [promptDraft, setPromptDraft] = useState('');
|
|
518
450
|
const [promptDraftOverride, setPromptDraftOverride] = useState(null);
|
|
519
451
|
const promptLayoutValueRef = useRef('');
|
|
520
452
|
const [, setPromptLayoutRows] = useState(1);
|
|
521
453
|
const [textEntryLayoutRows, setTextEntryLayoutRows] = useState(1);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
454
|
+
// Pasted image/text buffers + their [ref-token] lifecycle:
|
|
455
|
+
// app/use-pasted-buffers.mjs.
|
|
456
|
+
const {
|
|
457
|
+
pastedImagesRef,
|
|
458
|
+
nextPastedImageIdRef,
|
|
459
|
+
pastedTextsRef,
|
|
460
|
+
nextPastedTextIdRef,
|
|
461
|
+
setPastedImages,
|
|
462
|
+
setPastedTexts,
|
|
463
|
+
installPastedImages,
|
|
464
|
+
clearPastedImagesSnapshot,
|
|
465
|
+
registerPastedImage,
|
|
466
|
+
installPastedTexts,
|
|
467
|
+
clearPastedTextsSnapshot,
|
|
468
|
+
registerPastedText,
|
|
469
|
+
} = usePastedBuffers();
|
|
530
470
|
const promptValueRef = useRef('');
|
|
531
471
|
const promptSelectionRef = useRef(null);
|
|
532
472
|
// [mixdog] Prompt-box mouse selection wiring. boxRect is the editable text
|
|
@@ -538,30 +478,31 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
538
478
|
const promptMouseSelectionRef = useRef(null);
|
|
539
479
|
const promptHistoryNavRef = useRef({ active: false, index: -1, seed: '', lastValue: '' });
|
|
540
480
|
const promptHistoryDraftChangeRef = useRef(false);
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// the prompt draft gains its first character (see onPromptDraftChange).
|
|
552
|
-
// Generic key/mouse events (arrows, wheel, terminal escape replies) used to
|
|
553
|
-
// dismiss it instantly, so the hint vanished before the user ever typed.
|
|
554
|
-
const welcomePromptHintVisibleRef = useRef(false);
|
|
555
|
-
const dismissWelcomePromptHint = useCallback(() => {
|
|
556
|
-
if (!welcomePromptHintVisibleRef.current) return;
|
|
557
|
-
setWelcomePromptHintDismissed((dismissed) => dismissed || true);
|
|
558
|
-
}, []);
|
|
481
|
+
// Transient hint band under the prompt: app/use-prompt-hint.mjs.
|
|
482
|
+
const {
|
|
483
|
+
promptHint,
|
|
484
|
+
promptHintTone,
|
|
485
|
+
promptHintTimerRef,
|
|
486
|
+
promptHintActiveRef,
|
|
487
|
+
showPromptHint,
|
|
488
|
+
showSelectionCopyHint,
|
|
489
|
+
clearPromptHint,
|
|
490
|
+
} = usePromptHint();
|
|
559
491
|
const toastErrorSignature = useMemo(() => (
|
|
560
492
|
(state.toasts || [])
|
|
561
493
|
.filter((toast) => toast?.tone === 'error')
|
|
562
494
|
.map((toast) => `${toast.id || ''}:${toast.text || ''}`)
|
|
563
495
|
.join('|')
|
|
564
496
|
), [state.toasts]);
|
|
497
|
+
// Welcome-screen starter tip + conditional setup hints:
|
|
498
|
+
// app/use-welcome-prompt-hint.mjs.
|
|
499
|
+
const {
|
|
500
|
+
welcomePromptHintDismissed,
|
|
501
|
+
conditionalWelcomePromptHint,
|
|
502
|
+
welcomePromptHintRef,
|
|
503
|
+
welcomePromptHintVisibleRef,
|
|
504
|
+
dismissWelcomePromptHint,
|
|
505
|
+
} = useWelcomePromptHint({ store, state, toastErrorSignature });
|
|
565
506
|
const [slashIndex, setSlashIndex] = useState(0);
|
|
566
507
|
const [slashDismissedFor, setSlashDismissedFor] = useState('');
|
|
567
508
|
const slashPaletteRef = useRef({ open: false, count: 0 });
|
|
@@ -621,160 +562,66 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
621
562
|
timer.unref?.();
|
|
622
563
|
return () => { alive = false; clearTimeout(timer); };
|
|
623
564
|
}, [store]);
|
|
624
|
-
//
|
|
625
|
-
// after the onboarding refs above (const-TDZ) — with later-defined openers
|
|
626
|
-
// (openProviderSetupPicker/openOutputStylePicker) threaded as lazy getters
|
|
627
|
-
// that resolve the live binding at call time.
|
|
628
|
-
const { onboardingWarnReopen, openOnboardingAuthStep } = createOnboardingSteps({
|
|
629
|
-
store,
|
|
630
|
-
setPicker,
|
|
631
|
-
setProviderPrompt,
|
|
632
|
-
setChannelPrompt,
|
|
633
|
-
setHookPrompt,
|
|
634
|
-
setSettingsPrompt,
|
|
635
|
-
setOnboardingActive,
|
|
636
|
-
onboardingRef,
|
|
637
|
-
providerModelsCacheRef,
|
|
638
|
-
onboardingPrefetchSeqRef,
|
|
639
|
-
openProviderSetupPicker: (...a) => openProviderSetupPicker(...a),
|
|
640
|
-
openThemePicker,
|
|
641
|
-
openOutputStylePicker: (...a) => openOutputStylePicker(...a),
|
|
642
|
-
});
|
|
565
|
+
// Picker/panel factories + slash dispatch: app/create-app-pickers.mjs.
|
|
643
566
|
const {
|
|
567
|
+
openThemePicker,
|
|
568
|
+
openEffortPicker,
|
|
569
|
+
openResumePicker,
|
|
570
|
+
openMemoryCorePicker,
|
|
571
|
+
openMcpServersPicker,
|
|
572
|
+
openMcpPicker,
|
|
573
|
+
openProjectSkillsPicker,
|
|
574
|
+
openSkillsPicker,
|
|
575
|
+
openSkillDetailPicker,
|
|
576
|
+
beginAddPlugin,
|
|
577
|
+
openPluginDetailPicker,
|
|
578
|
+
openInstalledPluginsPicker,
|
|
579
|
+
openPluginsPicker,
|
|
580
|
+
openHooksPicker,
|
|
581
|
+
openUpdatePicker,
|
|
582
|
+
openAutoClearPicker,
|
|
583
|
+
openProfilePicker,
|
|
584
|
+
onboardingWarnReopen,
|
|
585
|
+
openOnboardingAuthStep,
|
|
644
586
|
openChannelTypeActionsPicker,
|
|
645
587
|
openChannelSettingTypePicker,
|
|
646
588
|
openChannelSetupPicker,
|
|
647
|
-
} = createChannelPickers({
|
|
648
|
-
store,
|
|
649
|
-
setPicker,
|
|
650
|
-
setProviderPrompt,
|
|
651
|
-
setChannelPrompt,
|
|
652
|
-
setHookPrompt,
|
|
653
|
-
setSettingsPrompt,
|
|
654
|
-
setContextPanel,
|
|
655
|
-
onboardingWarnReopen,
|
|
656
|
-
});
|
|
657
|
-
const { openProviderSetupPicker } = createProviderSetupPicker({
|
|
658
|
-
store,
|
|
659
|
-
setPicker,
|
|
660
|
-
setProviderPrompt,
|
|
661
|
-
setChannelPrompt,
|
|
662
|
-
setHookPrompt,
|
|
663
|
-
setSettingsPrompt,
|
|
664
|
-
setContextPanel,
|
|
665
|
-
closeUsagePanel,
|
|
666
|
-
oauthSubmitRef,
|
|
667
|
-
clearModelCaches,
|
|
668
|
-
});
|
|
669
|
-
const { openModelPicker } = createModelPicker({
|
|
670
|
-
store,
|
|
671
|
-
getState: () => state,
|
|
672
|
-
setPicker,
|
|
673
|
-
setProviderPrompt,
|
|
674
|
-
setChannelPrompt,
|
|
675
|
-
setHookPrompt,
|
|
676
|
-
setSettingsPrompt,
|
|
677
|
-
providerModelsCacheRef,
|
|
678
|
-
searchModelsCacheRef,
|
|
679
|
-
modelPickerRequestRef,
|
|
680
|
-
clearModelCaches,
|
|
681
|
-
modelSwitchNotice,
|
|
682
589
|
openProviderSetupPicker,
|
|
683
|
-
|
|
684
|
-
const {
|
|
590
|
+
openModelPicker,
|
|
685
591
|
openSearchPicker,
|
|
686
592
|
openAgentsPicker,
|
|
687
593
|
openWorkflowPicker,
|
|
688
594
|
openOutputStylePicker,
|
|
689
|
-
|
|
690
|
-
|
|
595
|
+
openSettingsPicker,
|
|
596
|
+
runSlashCommand,
|
|
597
|
+
} = createAppPickers({
|
|
691
598
|
state,
|
|
692
|
-
setPicker,
|
|
693
|
-
setProviderPrompt,
|
|
694
|
-
setChannelPrompt,
|
|
695
|
-
setHookPrompt,
|
|
696
|
-
setSettingsPrompt,
|
|
697
|
-
setContextPanel,
|
|
698
|
-
closeUsagePanel,
|
|
699
|
-
clean,
|
|
700
|
-
routeLabel,
|
|
701
|
-
agentModelParts,
|
|
702
|
-
agentModelProfile,
|
|
703
|
-
workflowSwitchNotice,
|
|
704
|
-
openModelPicker: (...a) => openModelPicker(...a),
|
|
705
|
-
});
|
|
706
|
-
const { openSettingsPicker } = createSettingsPicker({
|
|
707
599
|
store,
|
|
708
|
-
state,
|
|
709
600
|
setPicker,
|
|
710
601
|
setProviderPrompt,
|
|
711
602
|
setChannelPrompt,
|
|
712
603
|
setHookPrompt,
|
|
713
604
|
setSettingsPrompt,
|
|
714
|
-
settingsHeavyCacheRef,
|
|
715
|
-
formatDuration,
|
|
716
|
-
displayModelName,
|
|
717
|
-
routeModelLabel,
|
|
718
|
-
workflowDisplayName,
|
|
719
|
-
workflowSwitchNotice,
|
|
720
|
-
themeNotice,
|
|
721
|
-
openModelPicker: (...a) => openModelPicker(...a),
|
|
722
|
-
openSearchPicker: (...a) => openSearchPicker(...a),
|
|
723
|
-
openAgentsPicker: (...a) => openAgentsPicker(...a),
|
|
724
|
-
openWorkflowPicker: (...a) => openWorkflowPicker(...a),
|
|
725
|
-
openOutputStylePicker: (...a) => openOutputStylePicker(...a),
|
|
726
|
-
openProviderSetupPicker: (...a) => openProviderSetupPicker(...a),
|
|
727
|
-
openThemePicker: (...a) => openThemePicker(...a),
|
|
728
|
-
openAutoClearPicker: (...a) => openAutoClearPicker(...a),
|
|
729
|
-
openProfilePicker: (...a) => openProfilePicker(...a),
|
|
730
|
-
openMcpPicker: (...a) => openMcpPicker(...a),
|
|
731
|
-
openPluginsPicker: (...a) => openPluginsPicker(...a),
|
|
732
|
-
openHooksPicker: (...a) => openHooksPicker(...a),
|
|
733
|
-
openSkillsPicker: (...a) => openSkillsPicker(...a),
|
|
734
|
-
openMemoryCorePicker: (...a) => openMemoryCorePicker(...a),
|
|
735
|
-
openUpdatePicker: (...a) => openUpdatePicker(...a),
|
|
736
|
-
openChannelSettingTypePicker: (...a) => openChannelSettingTypePicker(...a),
|
|
737
|
-
});
|
|
738
|
-
const { runSlashCommand } = createSlashDispatch({
|
|
739
|
-
state,
|
|
740
|
-
store,
|
|
741
|
-
normalizeSlashCommandName,
|
|
742
605
|
setContextPanel,
|
|
606
|
+
setOnboardingActive,
|
|
743
607
|
closeUsagePanel,
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
enterProject
|
|
756
|
-
openProjectPicker
|
|
757
|
-
|
|
758
|
-
openSkillsPicker: (...a) => openSkillsPicker(...a),
|
|
759
|
-
openPluginsPicker: (...a) => openPluginsPicker(...a),
|
|
760
|
-
openHooksPicker: (...a) => openHooksPicker(...a),
|
|
761
|
-
openProviderSetupPicker: (...a) => openProviderSetupPicker(...a),
|
|
762
|
-
openChannelSetupPicker: (...a) => openChannelSetupPicker(...a),
|
|
763
|
-
openMemoryCorePicker: (...a) => openMemoryCorePicker(...a),
|
|
764
|
-
parseMemoryCommand,
|
|
765
|
-
openSettingsPicker: (...a) => openSettingsPicker(...a),
|
|
766
|
-
openAutoClearPicker: (...a) => openAutoClearPicker(...a),
|
|
767
|
-
formatDuration,
|
|
768
|
-
openResumePicker: (...a) => openResumePicker(...a),
|
|
608
|
+
oauthSubmitRef,
|
|
609
|
+
clearModelCaches,
|
|
610
|
+
onboardingRef,
|
|
611
|
+
providerModelsCacheRef,
|
|
612
|
+
searchModelsCacheRef,
|
|
613
|
+
modelPickerRequestRef,
|
|
614
|
+
onboardingPrefetchSeqRef,
|
|
615
|
+
settingsHeavyCacheRef,
|
|
616
|
+
livePickerRef,
|
|
617
|
+
disabledSkills,
|
|
618
|
+
setDisabledSkills,
|
|
619
|
+
enterProject,
|
|
620
|
+
openProjectPicker,
|
|
621
|
+
requestExit: (...a) => requestExit(...a),
|
|
769
622
|
openUsagePanel: (...a) => openUsagePanel(...a),
|
|
770
623
|
openContextPicker: (...a) => openContextPicker(...a),
|
|
771
|
-
openProfilePicker: (...a) => openProfilePicker(...a),
|
|
772
|
-
openUpdatePicker: (...a) => openUpdatePicker(...a),
|
|
773
|
-
runDoctor: (...a) => store.runDoctor?.(...a),
|
|
774
|
-
requestExit: (...a) => requestExit(...a),
|
|
775
624
|
});
|
|
776
|
-
const promptHintTimerRef = useRef(null);
|
|
777
|
-
const promptHintActiveRef = useRef(false);
|
|
778
625
|
// dragRef tracks an in-progress mouse text selection (see the mouse handler):
|
|
779
626
|
// anchor = where the drag began, last = the latest cell, active = button held.
|
|
780
627
|
// region: which surface the in-progress (or last) selection belongs to —
|
|
@@ -814,65 +661,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
814
661
|
const cols = Math.max(1, frameColumns - 4 - stringWidth('Sentence > '));
|
|
815
662
|
setTextEntryLayoutRows(textEntryReservedRows(settingsPrompt?.initialValue, cols, 8));
|
|
816
663
|
}, [settingsPrompt?.kind, settingsPrompt?.initialValue, frameColumns]);
|
|
817
|
-
useEffect(() => {
|
|
818
|
-
let alive = true;
|
|
819
|
-
const refreshConditionalWelcomeHint = async () => {
|
|
820
|
-
let next = '';
|
|
821
|
-
try {
|
|
822
|
-
const setup = await store.getProviderSetup?.();
|
|
823
|
-
if (setup && !providerSetupHasUsableProvider(setup)) {
|
|
824
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.noProvider;
|
|
825
|
-
}
|
|
826
|
-
} catch {
|
|
827
|
-
// If provider setup probing fails, let the generic/error tip path decide.
|
|
828
|
-
}
|
|
829
|
-
if (!next) {
|
|
830
|
-
const activeProvider = String(state.provider || '').trim();
|
|
831
|
-
const activeModel = String(state.model || '').trim();
|
|
832
|
-
if (!activeProvider || !activeModel) {
|
|
833
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.noModel;
|
|
834
|
-
} else {
|
|
835
|
-
try {
|
|
836
|
-
const models = await Promise.resolve(store.listProviderModels?.({ quick: true }) || []);
|
|
837
|
-
if (Array.isArray(models) && models.length === 0) {
|
|
838
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.noModel;
|
|
839
|
-
}
|
|
840
|
-
} catch {
|
|
841
|
-
// Model probing is advisory only; avoid replacing the random hint on failure.
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
const activeWorkflow = activeWorkflowSummaryForStore(store, state.workflow || {});
|
|
846
|
-
if (!next && String(activeWorkflow?.id || state.workflow?.id || '').toLowerCase() === 'solo') {
|
|
847
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.soloWorkflow;
|
|
848
|
-
}
|
|
849
|
-
if (!next) {
|
|
850
|
-
const searchRoute = store.getSearchRoute?.() || null;
|
|
851
|
-
const searchProvider = String(searchRoute?.provider || '').trim();
|
|
852
|
-
const searchModel = String(searchRoute?.model || '').trim();
|
|
853
|
-
const defaultSearchRoute = searchProvider.toLowerCase() === 'default' && searchModel.toLowerCase() === 'default';
|
|
854
|
-
if (defaultSearchRoute) {
|
|
855
|
-
try {
|
|
856
|
-
const models = await Promise.resolve(store.listProviderModels?.({ quick: true }) || []);
|
|
857
|
-
const current = Array.isArray(models)
|
|
858
|
-
? models.find((model) => model?.provider === state.provider && model?.id === state.model)
|
|
859
|
-
: null;
|
|
860
|
-
if (current && current.supportsWebSearch !== true) {
|
|
861
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.searchDefaultUnsupported;
|
|
862
|
-
}
|
|
863
|
-
} catch {
|
|
864
|
-
// Search default probing is advisory only.
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
if (!next && toastErrorSignature) {
|
|
869
|
-
next = CONDITIONAL_WELCOME_PROMPT_HINTS.error;
|
|
870
|
-
}
|
|
871
|
-
if (alive) setConditionalWelcomePromptHint((prev) => (prev === next ? prev : next));
|
|
872
|
-
};
|
|
873
|
-
void refreshConditionalWelcomeHint();
|
|
874
|
-
return () => { alive = false; };
|
|
875
|
-
}, [store, state.provider, state.model, state.workflow?.id, toastErrorSignature]);
|
|
876
664
|
const selectionLayoutRef = useRef(null);
|
|
877
665
|
const selectionTextRef = useRef('');
|
|
878
666
|
// lastClickRef tracks the previous left-press cell + time so the mouse handler
|
|
@@ -883,19 +671,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
883
671
|
// press resets to a fresh single.
|
|
884
672
|
const lastClickRef = useRef({ x: -1, y: -1, t: 0, count: 0 });
|
|
885
673
|
|
|
886
|
-
const showSelectionCopyHint = useCallback((text, tone = 'plain') => {
|
|
887
|
-
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
888
|
-
promptHintActiveRef.current = true;
|
|
889
|
-
setPromptHint(String(text || ''));
|
|
890
|
-
setPromptHintTone(tone);
|
|
891
|
-
promptHintTimerRef.current = setTimeout(() => {
|
|
892
|
-
promptHintTimerRef.current = null;
|
|
893
|
-
promptHintActiveRef.current = false;
|
|
894
|
-
setPromptHint('');
|
|
895
|
-
setPromptHintTone('info');
|
|
896
|
-
}, 2200);
|
|
897
|
-
}, []);
|
|
898
|
-
|
|
899
674
|
// ── Post-mount input gate ──────────────────────────────────────────────
|
|
900
675
|
// Let one event-loop poll pass so Ink processes (and discards, because
|
|
901
676
|
// PromptInput is still disabled) any keystrokes queued during boot/first
|
|
@@ -905,155 +680,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
905
680
|
return () => clearTimeout(timer);
|
|
906
681
|
}, []);
|
|
907
682
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
let trailing = null;
|
|
911
|
-
// Leading-edge fire on the first event of a burst, then coalesce the rest:
|
|
912
|
-
// events arriving within DEBOUNCE_MS skip the immediate full-layout update
|
|
913
|
-
// and only refresh the (reset) trailing timer, so a resize storm commits at
|
|
914
|
-
// most once per window plus one settle update instead of once per event.
|
|
915
|
-
const DEBOUNCE_MS = 80;
|
|
916
|
-
let lastRun = 0;
|
|
917
|
-
const update = () => {
|
|
918
|
-
setResizeState((prev) => {
|
|
919
|
-
const next = terminalSize(stdout);
|
|
920
|
-
// No-op when dimensions are unchanged: the unconditional post-mount
|
|
921
|
-
// update() otherwise forces an extra full-frame commit (epoch bump)
|
|
922
|
-
// right after the first paint, which reads as a boot flicker.
|
|
923
|
-
if (next.columns === prev.columns && next.rows === prev.rows) return prev;
|
|
924
|
-
return {
|
|
925
|
-
...next,
|
|
926
|
-
epoch: prev.epoch + 1,
|
|
927
|
-
};
|
|
928
|
-
});
|
|
929
|
-
};
|
|
930
|
-
const onResize = () => {
|
|
931
|
-
const now = Date.now();
|
|
932
|
-
if (now - lastRun >= DEBOUNCE_MS) {
|
|
933
|
-
lastRun = now;
|
|
934
|
-
update();
|
|
935
|
-
}
|
|
936
|
-
if (trailing) clearTimeout(trailing);
|
|
937
|
-
trailing = setTimeout(() => {
|
|
938
|
-
trailing = null;
|
|
939
|
-
lastRun = Date.now();
|
|
940
|
-
update();
|
|
941
|
-
}, DEBOUNCE_MS);
|
|
942
|
-
};
|
|
943
|
-
stdout.on('resize', onResize);
|
|
944
|
-
update();
|
|
945
|
-
return () => {
|
|
946
|
-
if (trailing) clearTimeout(trailing);
|
|
947
|
-
stdout.off('resize', onResize);
|
|
948
|
-
};
|
|
949
|
-
}, [stdout]);
|
|
950
|
-
|
|
951
|
-
const clearPromptHint = useCallback(() => {
|
|
952
|
-
if (!promptHintActiveRef.current && !promptHintTimerRef.current) return;
|
|
953
|
-
if (promptHintTimerRef.current) {
|
|
954
|
-
clearTimeout(promptHintTimerRef.current);
|
|
955
|
-
promptHintTimerRef.current = null;
|
|
956
|
-
}
|
|
957
|
-
promptHintActiveRef.current = false;
|
|
958
|
-
setPromptHint('');
|
|
959
|
-
setPromptHintTone('info');
|
|
960
|
-
}, []);
|
|
961
|
-
|
|
962
|
-
const showPromptHint = useCallback((text, tone = 'info') => {
|
|
963
|
-
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
964
|
-
promptHintActiveRef.current = true;
|
|
965
|
-
setPromptHint(String(text || ''));
|
|
966
|
-
setPromptHintTone(tone);
|
|
967
|
-
promptHintTimerRef.current = setTimeout(() => {
|
|
968
|
-
promptHintTimerRef.current = null;
|
|
969
|
-
promptHintActiveRef.current = false;
|
|
970
|
-
setPromptHint('');
|
|
971
|
-
setPromptHintTone('info');
|
|
972
|
-
}, 2200);
|
|
973
|
-
}, []);
|
|
974
|
-
|
|
975
|
-
const installPastedImages = useCallback((images, { merge = true } = {}) => {
|
|
976
|
-
if (!images || typeof images !== 'object' || Object.keys(images).length === 0) return;
|
|
977
|
-
const next = merge ? { ...pastedImagesRef.current, ...images } : { ...images };
|
|
978
|
-
pastedImagesRef.current = next;
|
|
979
|
-
const maxId = Object.keys(next)
|
|
980
|
-
.map((id) => Number(id) || 0)
|
|
981
|
-
.reduce((max, id) => Math.max(max, id), 0);
|
|
982
|
-
if (maxId >= nextPastedImageIdRef.current) nextPastedImageIdRef.current = maxId + 1;
|
|
983
|
-
setPastedImages(next);
|
|
984
|
-
}, []);
|
|
985
|
-
|
|
986
|
-
const clearPastedImagesSnapshot = useCallback((snapshot = null) => {
|
|
987
|
-
if (!snapshot) {
|
|
988
|
-
if (Object.keys(pastedImagesRef.current || {}).length === 0) return;
|
|
989
|
-
pastedImagesRef.current = {};
|
|
990
|
-
setPastedImages({});
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
994
|
-
const next = { ...pastedImagesRef.current };
|
|
995
|
-
let changed = false;
|
|
996
|
-
for (const [id, image] of Object.entries(snapshot)) {
|
|
997
|
-
if (next[id] === image) {
|
|
998
|
-
delete next[id];
|
|
999
|
-
changed = true;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
if (!changed) return;
|
|
1003
|
-
pastedImagesRef.current = next;
|
|
1004
|
-
setPastedImages(next);
|
|
1005
|
-
}, []);
|
|
1006
|
-
|
|
1007
|
-
const registerPastedImage = useCallback((image) => {
|
|
1008
|
-
if (!image || image.type !== 'image' || !image.content) return '';
|
|
1009
|
-
const id = nextPastedImageIdRef.current++;
|
|
1010
|
-
const entry = { ...image, id };
|
|
1011
|
-
pastedImagesRef.current = { ...pastedImagesRef.current, [id]: entry };
|
|
1012
|
-
setPastedImages(pastedImagesRef.current);
|
|
1013
|
-
return formatImageRef(id);
|
|
1014
|
-
}, []);
|
|
1015
|
-
|
|
1016
|
-
const installPastedTexts = useCallback((texts, { merge = true } = {}) => {
|
|
1017
|
-
if (!texts || typeof texts !== 'object' || Object.keys(texts).length === 0) return;
|
|
1018
|
-
const next = merge ? { ...pastedTextsRef.current, ...texts } : { ...texts };
|
|
1019
|
-
pastedTextsRef.current = next;
|
|
1020
|
-
const maxId = Object.keys(next)
|
|
1021
|
-
.map((id) => Number(id) || 0)
|
|
1022
|
-
.reduce((max, id) => Math.max(max, id), 0);
|
|
1023
|
-
if (maxId >= nextPastedTextIdRef.current) nextPastedTextIdRef.current = maxId + 1;
|
|
1024
|
-
setPastedTexts(next);
|
|
1025
|
-
}, []);
|
|
1026
|
-
|
|
1027
|
-
const clearPastedTextsSnapshot = useCallback((snapshot = null) => {
|
|
1028
|
-
if (!snapshot) {
|
|
1029
|
-
if (Object.keys(pastedTextsRef.current || {}).length === 0) return;
|
|
1030
|
-
pastedTextsRef.current = {};
|
|
1031
|
-
setPastedTexts({});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
1035
|
-
const next = { ...pastedTextsRef.current };
|
|
1036
|
-
let changed = false;
|
|
1037
|
-
for (const [id, text] of Object.entries(snapshot)) {
|
|
1038
|
-
if (next[id] === text) {
|
|
1039
|
-
delete next[id];
|
|
1040
|
-
changed = true;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
if (!changed) return;
|
|
1044
|
-
pastedTextsRef.current = next;
|
|
1045
|
-
setPastedTexts(next);
|
|
1046
|
-
}, []);
|
|
1047
|
-
|
|
1048
|
-
const registerPastedText = useCallback((text) => {
|
|
1049
|
-
const value = String(text ?? '');
|
|
1050
|
-
if (!value) return '';
|
|
1051
|
-
const id = nextPastedTextIdRef.current++;
|
|
1052
|
-
const entry = { id, text: value };
|
|
1053
|
-
pastedTextsRef.current = { ...pastedTextsRef.current, [id]: entry };
|
|
1054
|
-
setPastedTexts(pastedTextsRef.current);
|
|
1055
|
-
return formatPastedTextRef(id, value);
|
|
1056
|
-
}, []);
|
|
683
|
+
// Resize debounce + extended-keyboard enable: app/use-terminal-chrome.mjs.
|
|
684
|
+
useTerminalChrome({ stdout, isRawModeSupported, setResizeState });
|
|
1057
685
|
|
|
1058
686
|
// Transcript scroll + grid-selection engine: extracted to app/use-transcript-scroll.mjs.
|
|
1059
687
|
const {
|
|
@@ -1092,60 +720,20 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1092
720
|
selectionTextRef,
|
|
1093
721
|
});
|
|
1094
722
|
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
// A selection that has partially scrolled out of the viewport renders —
|
|
1105
|
-
// and therefore harvests — only its visible rows. The remembered text
|
|
1106
|
-
// (captured while the selection was last fully painted) is the fuller
|
|
1107
|
-
// copy; prefer whichever is longer so scrolling never shrinks a copy.
|
|
1108
|
-
let text = renderText == null
|
|
1109
|
-
? remembered
|
|
1110
|
-
: (remembered.length > renderText.length ? remembered : renderText);
|
|
1111
|
-
// The stitch buffer accumulates rows harvested across every scroll position
|
|
1112
|
-
// during a transcript drag, so it can reconstruct rows that scrolled out of
|
|
1113
|
-
// view entirely (neither renderText nor the last-full-paint remembered text
|
|
1114
|
-
// ever saw them). getStitchedSelectionText now reports a `complete` flag:
|
|
1115
|
-
// prefer the stitch ONLY when it contiguously covers the selection (no
|
|
1116
|
-
// interior gap) AND adds rows. A gapped stitch silently drops a scrolled-off
|
|
1117
|
-
// row, so preferring it purely on length yielded a mangled copy.
|
|
1118
|
-
const stitched = getStitchedSelectionText?.() || { text: '', complete: false };
|
|
1119
|
-
if (stitched.complete && stitched.text.length > text.length) text = stitched.text;
|
|
1120
|
-
if ((!text || !text.trim()) && attempt < 4) {
|
|
1121
|
-
setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
if (!text || !text.trim()) {
|
|
1125
|
-
// Retries exhausted with nothing to copy: never return silently — the
|
|
1126
|
-
// user pressed Ctrl+C expecting feedback. Surface a hint (and still
|
|
1127
|
-
// swallow the key, which the caller already did).
|
|
1128
|
-
showSelectionCopyHint('nothing to copy · select text first', 'error');
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
selectionTextRef.current = text;
|
|
1132
|
-
copyToClipboard(text)
|
|
1133
|
-
.then(() => {
|
|
1134
|
-
const lines = text.split('\n').length;
|
|
1135
|
-
const chars = text.length;
|
|
1136
|
-
showSelectionCopyHint(`copied ${chars} char${chars === 1 ? '' : 's'}${lines > 1 ? ` · ${lines} lines` : ''}`, 'plain');
|
|
1137
|
-
})
|
|
1138
|
-
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
1139
|
-
}, [store, showSelectionCopyHint, getStitchedSelectionText]);
|
|
723
|
+
// Ctrl+C selection copy (render/remembered/stitch merge + retry + hint):
|
|
724
|
+
// app/use-copy-selection.mjs. Declared after useTranscriptScroll so
|
|
725
|
+
// getStitchedSelectionText exists (TDZ).
|
|
726
|
+
const { copySelection } = useCopySelection({
|
|
727
|
+
store,
|
|
728
|
+
selectionTextRef,
|
|
729
|
+
getStitchedSelectionText,
|
|
730
|
+
showSelectionCopyHint,
|
|
731
|
+
});
|
|
1140
732
|
|
|
1141
733
|
useEffect(() => () => {
|
|
1142
734
|
stopSmoothScroll();
|
|
1143
735
|
}, [stopSmoothScroll]);
|
|
1144
736
|
|
|
1145
|
-
useEffect(() => () => {
|
|
1146
|
-
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
1147
|
-
}, []);
|
|
1148
|
-
|
|
1149
737
|
// SGR mouse handling: extracted to app/use-mouse-input.mjs (useMouseInput).
|
|
1150
738
|
useMouseInput({
|
|
1151
739
|
inkInput,
|
|
@@ -1175,26 +763,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1175
763
|
clearStitchBuffer,
|
|
1176
764
|
});
|
|
1177
765
|
|
|
1178
|
-
// Enable extended keyboard reporting (kitty + xterm modifyOtherKeys)
|
|
1179
|
-
// SYNCHRONOUSLY, ONCE, with NO query/round-trip. ink
|
|
1180
|
-
// turns raw mode on during the first useInput mount (synchronously, inside
|
|
1181
|
-
// render); this mount effect runs in the same commit phase, right after — i.e.
|
|
1182
|
-
// before the user can realistically press a key. We write BOTH enables
|
|
1183
|
-
// unconditionally (the terminal honors whichever it implements; Windows
|
|
1184
|
-
// Terminal 1.24 has no kitty but DOES honor modifyOtherKeys), gated only by the
|
|
1185
|
-
// supportsExtendedKeys() allowlist. Because the enable lands before the first
|
|
1186
|
-
// keypress is read, the FIRST Ctrl+Enter already arrives as a distinguishable
|
|
1187
|
-
// \x1b[27;5;13~ (or kitty \x1b[13;5u) instead of a bare \r — fixing the old
|
|
1188
|
-
// "first Ctrl+Enter submits, second works" race. Teardown lives in index.jsx's
|
|
1189
|
-
// restoreTerminal(). The empty dep array makes this run exactly once on mount.
|
|
1190
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1191
|
-
useEffect(() => {
|
|
1192
|
-
if (!isRawModeSupported || !stdout?.write) return;
|
|
1193
|
-
if (!supportsExtendedKeys()) return;
|
|
1194
|
-
try {
|
|
1195
|
-
stdout.write(ENABLE_KITTY_KEYBOARD + ENABLE_MODIFY_OTHER_KEYS);
|
|
1196
|
-
} catch { /* terminal may be closing */ }
|
|
1197
|
-
}, []);
|
|
1198
766
|
|
|
1199
767
|
// Item-count changes are the only time we can arm follow before row totals are
|
|
1200
768
|
// recomputed. Pure streaming height growth is handled in the row-delta effect.
|
|
@@ -1210,79 +778,28 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1210
778
|
if (scrollTargetRef.current === 0 || followingRef.current) followingRef.current = true;
|
|
1211
779
|
}, [state.items.length, resetTranscriptScroll]);
|
|
1212
780
|
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
1236
|
-
}, 60);
|
|
1237
|
-
}, [store, exit]);
|
|
1238
|
-
|
|
1239
|
-
const restoreQueuedToPrompt = useCallback((options = {}) => {
|
|
1240
|
-
const restoreDraft = options.restoreDraft !== false;
|
|
1241
|
-
const showHint = options.showHint !== false;
|
|
1242
|
-
const currentText = options.currentText ?? promptValueRef.current ?? promptDraft;
|
|
1243
|
-
const restored = store.restoreQueued?.(currentText);
|
|
1244
|
-
if (!restored || restored.count === 0) {
|
|
1245
|
-
if (showHint) showPromptHint('No queued messages to restore.', 'info');
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
if (restoreDraft) {
|
|
1249
|
-
if (restored.pastedImages) installPastedImages(restored.pastedImages, { merge: true });
|
|
1250
|
-
if (restored.pastedTexts) installPastedTexts(restored.pastedTexts, { merge: true });
|
|
1251
|
-
syncPromptLayoutRows(restored.text);
|
|
1252
|
-
setPromptDraftOverride({ id: Date.now(), value: restored.text });
|
|
1253
|
-
}
|
|
1254
|
-
if (showHint) {
|
|
1255
|
-
showPromptHint(`restored ${restored.count} queued message${restored.count === 1 ? '' : 's'}`, 'info');
|
|
1256
|
-
} else {
|
|
1257
|
-
clearPromptHint();
|
|
1258
|
-
}
|
|
1259
|
-
return true;
|
|
1260
|
-
}, [store, promptDraft, showPromptHint, clearPromptHint, installPastedImages, syncPromptLayoutRows]);
|
|
1261
|
-
|
|
1262
|
-
const recentPromptHistory = useMemo(() => {
|
|
1263
|
-
// The engine maintains this list incrementally (rebuilt only when a user
|
|
1264
|
-
// item is appended or the transcript is bulk-swapped), so App no longer
|
|
1265
|
-
// rescans all items on every transcript change. Fall back to a local scan
|
|
1266
|
-
// only if the engine did not publish it (older snapshot).
|
|
1267
|
-
if (Array.isArray(state.promptHistoryList)) return state.promptHistoryList;
|
|
1268
|
-
const items = Array.isArray(state.items) ? state.items : [];
|
|
1269
|
-
const seen = new Set();
|
|
1270
|
-
const history = [];
|
|
1271
|
-
for (let i = items.length - 1; i >= 0 && history.length < PROMPT_HISTORY_LIMIT; i -= 1) {
|
|
1272
|
-
const item = items[i];
|
|
1273
|
-
if (item?.kind !== 'user') continue;
|
|
1274
|
-
const text = String(item.text || '').trim();
|
|
1275
|
-
const key = promptHistoryKey(text);
|
|
1276
|
-
if (!key || seen.has(key)) continue;
|
|
1277
|
-
seen.add(key);
|
|
1278
|
-
history.push(text);
|
|
1279
|
-
}
|
|
1280
|
-
return history;
|
|
1281
|
-
}, [state.promptHistoryList, state.items]);
|
|
1282
|
-
|
|
1283
|
-
const resetPromptHistoryNav = useCallback(() => {
|
|
1284
|
-
promptHistoryNavRef.current = { active: false, index: -1, seed: '', lastValue: '' };
|
|
1285
|
-
}, []);
|
|
781
|
+
// Exit + queued-restore + prompt history: app/use-prompt-queue-history.mjs.
|
|
782
|
+
const {
|
|
783
|
+
requestExit,
|
|
784
|
+
restoreQueuedToPrompt,
|
|
785
|
+
recentPromptHistory,
|
|
786
|
+
resetPromptHistoryNav,
|
|
787
|
+
} = usePromptQueueHistory({
|
|
788
|
+
store,
|
|
789
|
+
state,
|
|
790
|
+
exit,
|
|
791
|
+
exitRequestedRef,
|
|
792
|
+
setExiting,
|
|
793
|
+
promptValueRef,
|
|
794
|
+
promptDraft,
|
|
795
|
+
showPromptHint,
|
|
796
|
+
clearPromptHint,
|
|
797
|
+
installPastedImages,
|
|
798
|
+
installPastedTexts,
|
|
799
|
+
syncPromptLayoutRows,
|
|
800
|
+
setPromptDraftOverride,
|
|
801
|
+
promptHistoryNavRef,
|
|
802
|
+
});
|
|
1286
803
|
|
|
1287
804
|
// PROMPT HANDLER cluster extracted to app/use-prompt-handlers.mjs.
|
|
1288
805
|
const {
|
|
@@ -1327,395 +844,48 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1327
844
|
setToolOutputExpanded((expanded) => !expanded);
|
|
1328
845
|
}, []);
|
|
1329
846
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
copyToClipboard(promptSelectionText)
|
|
1357
|
-
.then(() => showSelectionCopyHint(`copied ${promptSelectionText.length} char${promptSelectionText.length === 1 ? '' : 's'}`, 'plain'))
|
|
1358
|
-
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
if (hasInkSelection) {
|
|
1362
|
-
copySelection();
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
// No app-owned selection. On Windows Terminal the same Ctrl+C is also the
|
|
1366
|
-
// native terminal's copy shortcut for a mouse selection we can't see — so
|
|
1367
|
-
// rendering a hint here fights that copy and flashes a spurious message.
|
|
1368
|
-
// Suppress the hint on win32 (interrupt routing is unchanged: Esc still
|
|
1369
|
-
// interrupts). Other platforms keep the guidance.
|
|
1370
|
-
if (process.platform !== 'win32') {
|
|
1371
|
-
showSelectionCopyHint('select text to copy · Esc interrupts', 'plain');
|
|
1372
|
-
}
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
1376
|
-
toggleExpand();
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
const rawShiftUp = input === '\x1b[1;2A' || input === '\x1b[a' || input === '[1;2A';
|
|
1380
|
-
const rawShiftDown = input === '\x1b[1;2B' || input === '\x1b[b' || input === '[1;2B';
|
|
1381
|
-
const rawShiftRight = input === '\x1b[1;2C' || input === '\x1b[c' || input === '[1;2C';
|
|
1382
|
-
const rawShiftLeft = input === '\x1b[1;2D' || input === '\x1b[d' || input === '[1;2D';
|
|
1383
|
-
const rawCtrlShiftUp = input === '\x1b[1;6A' || input === '[1;6A';
|
|
1384
|
-
const rawCtrlShiftDown = input === '\x1b[1;6B' || input === '[1;6B';
|
|
1385
|
-
const rawCtrlShiftRight = input === '\x1b[1;6C' || input === '[1;6C';
|
|
1386
|
-
const rawCtrlShiftLeft = input === '\x1b[1;6D' || input === '[1;6D';
|
|
1387
|
-
const rawModifiedShiftArrow = rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
|
|
1388
|
-
|| rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
|
|
1389
|
-
if (
|
|
1390
|
-
!picker
|
|
1391
|
-
&& (
|
|
1392
|
-
(key.shift && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end))
|
|
1393
|
-
|| rawModifiedShiftArrow
|
|
1394
|
-
)
|
|
1395
|
-
) {
|
|
1396
|
-
// Consume the chord whenever a transcript/status ink-grid selection is
|
|
1397
|
-
// live — even if the focus clamps at an edge (moveSelectionFocus returns
|
|
1398
|
-
// false there). PromptInput independently skips the same chord via the
|
|
1399
|
-
// shared gridSelectionActiveRef predicate, so there is no double-handling.
|
|
1400
|
-
// When no grid selection is live, fall through to PromptInput.
|
|
1401
|
-
let move = null;
|
|
1402
|
-
if (key.leftArrow || rawShiftLeft || rawCtrlShiftLeft) move = 'left';
|
|
1403
|
-
else if (key.rightArrow || rawShiftRight || rawCtrlShiftRight) move = 'right';
|
|
1404
|
-
else if (key.upArrow || rawShiftUp || rawCtrlShiftUp) move = 'up';
|
|
1405
|
-
else if (key.downArrow || rawShiftDown || rawCtrlShiftDown) move = 'down';
|
|
1406
|
-
else if (key.home) move = 'lineStart';
|
|
1407
|
-
else if (key.end) move = 'lineEnd';
|
|
1408
|
-
if (move && gridSelectionActiveRef.current()) {
|
|
1409
|
-
moveSelectionFocus(move);
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
if (key.escape && usagePanel && !picker) {
|
|
1414
|
-
closeUsagePanel();
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
if (key.escape && contextPanel && !picker) {
|
|
1418
|
-
setContextPanel(null);
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
if (key.pageUp) {
|
|
1422
|
-
if (overlayBlocksGlobalTranscriptScroll(scrollFocusRef.current)) return;
|
|
1423
|
-
const pageRows = Math.max(3, Math.floor((resizeState.rows ?? 24) * 0.6));
|
|
1424
|
-
scrollTranscriptRows(pageRows);
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
if (key.pageDown) {
|
|
1428
|
-
if (overlayBlocksGlobalTranscriptScroll(scrollFocusRef.current)) return;
|
|
1429
|
-
const pageRows = Math.max(3, Math.floor((resizeState.rows ?? 24) * 0.6));
|
|
1430
|
-
scrollTranscriptRows(-pageRows);
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
if (key.ctrl && key.end) {
|
|
1434
|
-
resetTranscriptScroll();
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
if (key.escape && !picker) {
|
|
1438
|
-
dragRef.current.active = false;
|
|
1439
|
-
dragRef.current.region = null;
|
|
1440
|
-
dragRef.current.anchorSpan = null;
|
|
1441
|
-
// Clear whichever region's selection is active. PromptInput's own ESC also
|
|
1442
|
-
// clears its selection when focused/enabled; this covers the disabled case
|
|
1443
|
-
// and a status/transcript ink-grid selection in one press.
|
|
1444
|
-
promptMouseSelectionRef.current?.clear?.();
|
|
1445
|
-
applySelectionRect(null);
|
|
1446
|
-
}
|
|
1447
|
-
}, { isActive: isRawModeSupported });
|
|
1448
|
-
|
|
1449
|
-
const openUsagePanel = (arg = '') => {
|
|
1450
|
-
const refresh = /(?:^|\s)(?:refresh|--refresh|-r|true)(?:\s|$)/i.test(String(arg || ''));
|
|
1451
|
-
const requestId = usageRequestRef.current + 1;
|
|
1452
|
-
usageRequestRef.current = requestId;
|
|
1453
|
-
setProviderPrompt(null);
|
|
1454
|
-
setChannelPrompt(null);
|
|
1455
|
-
setHookPrompt(null);
|
|
1456
|
-
setSettingsPrompt(null);
|
|
1457
|
-
setPicker(null);
|
|
1458
|
-
setContextPanel(null);
|
|
1459
|
-
setUsagePanel({
|
|
1460
|
-
title: 'Provider Quotas',
|
|
1461
|
-
subtitle: 'Statusline-style provider quota windows.',
|
|
1462
|
-
checking: true,
|
|
1463
|
-
refresh,
|
|
1464
|
-
rows: [],
|
|
1465
|
-
total: null,
|
|
1466
|
-
});
|
|
1467
|
-
setTimeout(() => {
|
|
1468
|
-
if (usageRequestRef.current !== requestId) return;
|
|
1469
|
-
void store.getUsageDashboard?.({
|
|
1470
|
-
refresh,
|
|
1471
|
-
onUpdate: (dashboard) => {
|
|
1472
|
-
if (usageRequestRef.current !== requestId) return;
|
|
1473
|
-
if (!dashboard) return;
|
|
1474
|
-
setUsagePanel(dashboard);
|
|
1475
|
-
},
|
|
1476
|
-
})
|
|
1477
|
-
.then((dashboard) => {
|
|
1478
|
-
if (usageRequestRef.current !== requestId) return;
|
|
1479
|
-
if (!dashboard) {
|
|
1480
|
-
closeUsagePanel();
|
|
1481
|
-
store.pushNotice('usage dashboard unavailable', 'warn');
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
setUsagePanel(dashboard);
|
|
1485
|
-
})
|
|
1486
|
-
.catch((e) => {
|
|
1487
|
-
if (usageRequestRef.current !== requestId) return;
|
|
1488
|
-
closeUsagePanel();
|
|
1489
|
-
store.pushNotice(`usage failed: ${e?.message || e}`, 'error');
|
|
1490
|
-
});
|
|
1491
|
-
}, 0);
|
|
1492
|
-
};
|
|
847
|
+
// App-level key handling (approval keys, copy-first Ctrl+C, grid-selection
|
|
848
|
+
// chords, panel Escapes, transcript paging): app/use-global-key-input.mjs.
|
|
849
|
+
useGlobalKeyInput({
|
|
850
|
+
store,
|
|
851
|
+
state,
|
|
852
|
+
toolApproval,
|
|
853
|
+
picker,
|
|
854
|
+
usagePanel,
|
|
855
|
+
contextPanel,
|
|
856
|
+
setContextPanel,
|
|
857
|
+
closeUsagePanel,
|
|
858
|
+
isRawModeSupported,
|
|
859
|
+
resizeState,
|
|
860
|
+
promptSelectionRef,
|
|
861
|
+
promptMouseSelectionRef,
|
|
862
|
+
dragRef,
|
|
863
|
+
scrollFocusRef,
|
|
864
|
+
gridSelectionActiveRef,
|
|
865
|
+
moveSelectionFocus,
|
|
866
|
+
copySelection,
|
|
867
|
+
showSelectionCopyHint,
|
|
868
|
+
toggleExpand,
|
|
869
|
+
scrollTranscriptRows,
|
|
870
|
+
resetTranscriptScroll,
|
|
871
|
+
applySelectionRect,
|
|
872
|
+
});
|
|
1493
873
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const schemaBreakdown = request.toolSchemaBreakdown || {};
|
|
1510
|
-
const schemaTokensFor = (buckets) => buckets.reduce(
|
|
1511
|
-
(sum, bucket) => sum + Number(schemaBreakdown?.[bucket]?.tokens || 0),
|
|
1512
|
-
0,
|
|
1513
|
-
);
|
|
1514
|
-
const builtInToolSchemaTokens = schemaTokensFor(['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
1515
|
-
const mcpToolSchemaTokens = schemaTokensFor(['mcp']);
|
|
1516
|
-
const compaction = context.compaction || {};
|
|
1517
|
-
const windowTokens = Number(context.contextWindow || state.contextWindow || context.rawContextWindow || state.rawContextWindow || 0);
|
|
1518
|
-
const rawWindowTokens = Number(context.rawContextWindow || state.rawContextWindow || windowTokens || 0);
|
|
1519
|
-
// Compaction boundary/trigger are sourced from the runtime contextStatus
|
|
1520
|
-
// (context.compaction). Fall back to the visible window for the boundary
|
|
1521
|
-
// and to the boundary for the trigger so /context still renders on a
|
|
1522
|
-
// fresh/resumed session before any compaction telemetry exists. (These
|
|
1523
|
-
// used to reference undefined compactTrigger/compactBoundary and threw a
|
|
1524
|
-
// ReferenceError when the picker opened.)
|
|
1525
|
-
const compactBoundary = Number(compaction.boundaryTokens || windowTokens || 0);
|
|
1526
|
-
const compactTrigger = Number(compaction.triggerTokens || compactBoundary || 0);
|
|
1527
|
-
const usedTokens = Number(context.usedTokens || context.currentEstimatedTokens || usage.lastContextTokens || 0);
|
|
1528
|
-
const freeTokens = windowTokens ? Math.max(0, windowTokens - usedTokens) : Number(context.freeTokens || 0);
|
|
1529
|
-
const pct = (value, total = windowTokens) => {
|
|
1530
|
-
const n = Number(value || 0);
|
|
1531
|
-
const d = Number(total || 0);
|
|
1532
|
-
if (!d) return 'N/A';
|
|
1533
|
-
const p = Math.max(0, Math.min(100, (n / d) * 100));
|
|
1534
|
-
return `${p > 0 && p < 1 ? p.toFixed(1) : Math.floor(p)}%`;
|
|
1535
|
-
};
|
|
1536
|
-
const fmt = (value) => {
|
|
1537
|
-
const n = Number(value || 0);
|
|
1538
|
-
if (!Number.isFinite(n) || n <= 0) return '0';
|
|
1539
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`;
|
|
1540
|
-
if (n >= 10_000) return `${Math.round(n / 1000)}k`;
|
|
1541
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
1542
|
-
return `${Math.round(n)}`;
|
|
1543
|
-
};
|
|
1544
|
-
const cachedRead = Number(usage.lastCachedReadTokens || 0);
|
|
1545
|
-
const cacheWrite = Number(usage.lastCacheWriteTokens || 0);
|
|
1546
|
-
const freshInput = Number(
|
|
1547
|
-
usage.lastUncachedInputTokens != null
|
|
1548
|
-
? usage.lastUncachedInputTokens
|
|
1549
|
-
: Math.max(Number(usage.lastInputTokens || 0) - cachedRead - cacheWrite, 0)
|
|
1550
|
-
);
|
|
1551
|
-
const cacheDenom = Number(usage.lastContextTokens || 0) || (cachedRead + freshInput + cacheWrite);
|
|
1552
|
-
const cacheHitRate = cacheDenom > 0
|
|
1553
|
-
? `${((cachedRead / cacheDenom) * 100).toFixed(0)}%`
|
|
1554
|
-
: 'N/A';
|
|
1555
|
-
const cacheWriteLabel = cacheWrite > 0 ? ` · ${fmt(cacheWrite)} write` : '';
|
|
1556
|
-
const contextSource = context.usedSource === 'last_api_request' ? 'last API request' : 'estimated';
|
|
1557
|
-
const lastApiLabel = context.lastApiRequestStale ? 'last API request (pre-compact)' : 'last API request';
|
|
1558
|
-
const compactElapsed = (value) => {
|
|
1559
|
-
const n = Number(value || 0);
|
|
1560
|
-
if (!Number.isFinite(n) || n <= 0) return '';
|
|
1561
|
-
return `${Math.max(1, Math.ceil(n / 1000))}s`;
|
|
1562
|
-
};
|
|
1563
|
-
const compactRunning = compaction.inProgress === true || compaction.lastStage === 'compacting';
|
|
1564
|
-
const autoClearFailed = compaction.lastStage === 'auto_clear_failed' || !!compaction.lastClearCompactError;
|
|
1565
|
-
const autoClearStage = compaction.lastStage === 'auto_clear' || compaction.lastClearAt;
|
|
1566
|
-
const compactDuration = compactElapsed(compaction.lastDurationMs);
|
|
1567
|
-
const compactInterrupted = compaction.lastStage === 'interrupted';
|
|
1568
|
-
const compactReactive = String(compaction.lastTrigger || '').toLowerCase() === 'reactive';
|
|
1569
|
-
const compactState = compactRunning
|
|
1570
|
-
? 'Compacting conversation'
|
|
1571
|
-
: compactInterrupted
|
|
1572
|
-
? 'Compact interrupted'
|
|
1573
|
-
: autoClearFailed
|
|
1574
|
-
? `auto-clear skipped${compaction.lastClearCompactError ? `: ${compaction.lastClearCompactError}` : ''}`
|
|
1575
|
-
: autoClearStage
|
|
1576
|
-
? 'Auto-clear complete'
|
|
1577
|
-
: compaction.lastChanged
|
|
1578
|
-
? (compactReactive ? 'Compact complete (overflow recovery)' : 'Compact complete')
|
|
1579
|
-
: 'Compact checked';
|
|
1580
|
-
const compactDescription = compactDuration
|
|
1581
|
-
? `${compactState} · ${compactDuration}`
|
|
1582
|
-
: compactState;
|
|
1583
|
-
const contextRows = [
|
|
1584
|
-
{
|
|
1585
|
-
value: 'summary',
|
|
1586
|
-
label: 'Context Usage',
|
|
1587
|
-
description: `${fmt(usedTokens)}/${fmt(windowTokens)} (${pct(usedTokens)}) · ${fmt(freeTokens)} free · ${contextSource} · effective`,
|
|
1588
|
-
_action: 'summary',
|
|
1589
|
-
},
|
|
1590
|
-
{
|
|
1591
|
-
value: 'compaction',
|
|
1592
|
-
label: 'Compaction',
|
|
1593
|
-
description: compactDescription,
|
|
1594
|
-
_action: 'compaction',
|
|
1595
|
-
},
|
|
1596
|
-
{
|
|
1597
|
-
value: 'messages',
|
|
1598
|
-
label: 'Messages',
|
|
1599
|
-
description: `${fmt(messages.estimatedTokens)} tokens (${pct(messages.estimatedTokens)}) · ${messages.count || 0} messages`,
|
|
1600
|
-
_action: 'messages',
|
|
1601
|
-
},
|
|
1602
|
-
{
|
|
1603
|
-
value: 'tools',
|
|
1604
|
-
label: 'Tools',
|
|
1605
|
-
description: `${fmt(builtInToolSchemaTokens)} schema tokens (${pct(builtInToolSchemaTokens)}) · ${tools.activeCount || 0}/${tools.count || 0} active`,
|
|
1606
|
-
_action: 'tools',
|
|
1607
|
-
},
|
|
1608
|
-
{
|
|
1609
|
-
value: 'tool-io',
|
|
1610
|
-
label: 'Tool calls/results',
|
|
1611
|
-
description: `${messages.toolCallCount || 0} calls (${fmt(messages.toolCallTokens)}) · ${messages.toolResultCount || 0} results (${fmt(messages.toolResultTokens)})`,
|
|
1612
|
-
_action: 'tool-io',
|
|
1613
|
-
},
|
|
1614
|
-
{
|
|
1615
|
-
value: 'request',
|
|
1616
|
-
label: 'Request overhead',
|
|
1617
|
-
description: `${fmt(request.requestOverheadTokens)} framing · ${fmt(request.reserveTokens)} reserve incl. tools`,
|
|
1618
|
-
_action: 'request',
|
|
1619
|
-
},
|
|
1620
|
-
{
|
|
1621
|
-
value: 'last-api',
|
|
1622
|
-
label: 'Last API usage',
|
|
1623
|
-
description: `${fmt(usage.lastContextTokens)} context · ${fmt(freshInput)} uncached input · ${fmt(usage.lastOutputTokens)} output · ${lastApiLabel}`,
|
|
1624
|
-
_action: 'last-api',
|
|
1625
|
-
},
|
|
1626
|
-
{
|
|
1627
|
-
value: 'cache',
|
|
1628
|
-
label: 'Prompt cache',
|
|
1629
|
-
description: `${cacheHitRate} hit · ${fmt(usage.lastCachedReadTokens)} read${cacheWriteLabel} · ${fmt(freshInput)} new (last request)`,
|
|
1630
|
-
_action: 'cache',
|
|
1631
|
-
},
|
|
1632
|
-
{
|
|
1633
|
-
value: 'free',
|
|
1634
|
-
label: 'Free space',
|
|
1635
|
-
description: `${fmt(freeTokens)} tokens (${pct(freeTokens)}) · raw window ${fmt(rawWindowTokens)}`,
|
|
1636
|
-
_action: 'free',
|
|
1637
|
-
},
|
|
1638
|
-
{
|
|
1639
|
-
value: 'extensions',
|
|
1640
|
-
label: 'Skills/plugins',
|
|
1641
|
-
description: `${skills.count || 0} skills · ${plugins.count || 0} plugins`,
|
|
1642
|
-
_action: 'extensions',
|
|
1643
|
-
},
|
|
1644
|
-
];
|
|
1645
|
-
setProviderPrompt(null);
|
|
1646
|
-
setChannelPrompt(null);
|
|
1647
|
-
setHookPrompt(null);
|
|
1648
|
-
setSettingsPrompt(null);
|
|
1649
|
-
setPicker(null);
|
|
1650
|
-
setContextPanel({
|
|
1651
|
-
kind: 'context',
|
|
1652
|
-
title: 'Context Usage',
|
|
1653
|
-
detail: {
|
|
1654
|
-
type: 'context',
|
|
1655
|
-
usage: {
|
|
1656
|
-
usedTokens,
|
|
1657
|
-
windowTokens,
|
|
1658
|
-
freeTokens,
|
|
1659
|
-
rawWindowTokens,
|
|
1660
|
-
source: contextSource,
|
|
1661
|
-
effective: true,
|
|
1662
|
-
},
|
|
1663
|
-
compaction: {
|
|
1664
|
-
stage: compaction.lastStage || 'pending',
|
|
1665
|
-
state: compactState,
|
|
1666
|
-
triggerTokens: compactTrigger,
|
|
1667
|
-
boundaryTokens: compactBoundary,
|
|
1668
|
-
type: compaction.compactType || compaction.type || null,
|
|
1669
|
-
bufferTokens: Number(compaction.bufferTokens || (compactBoundary && compactTrigger ? Math.max(0, compactBoundary - compactTrigger) : 0)) || null,
|
|
1670
|
-
pressureTokens: Number(compaction.lastPressureTokens || compaction.currentEstimatedTokens || 0) || null,
|
|
1671
|
-
lastChanged: compaction.lastChanged === true,
|
|
1672
|
-
},
|
|
1673
|
-
messages: {
|
|
1674
|
-
tokens: messages.estimatedTokens,
|
|
1675
|
-
count: messages.count,
|
|
1676
|
-
semantic: messages.semantic,
|
|
1677
|
-
},
|
|
1678
|
-
tools: {
|
|
1679
|
-
schemaTokens: builtInToolSchemaTokens,
|
|
1680
|
-
active: tools.activeCount,
|
|
1681
|
-
count: tools.count,
|
|
1682
|
-
},
|
|
1683
|
-
toolIo: {
|
|
1684
|
-
calls: messages.toolCallCount,
|
|
1685
|
-
results: messages.toolResultCount,
|
|
1686
|
-
},
|
|
1687
|
-
request: {
|
|
1688
|
-
toolSchemaBreakdown: request.toolSchemaBreakdown,
|
|
1689
|
-
overheadTokens: request.requestOverheadTokens,
|
|
1690
|
-
reserveTokens: request.reserveTokens,
|
|
1691
|
-
},
|
|
1692
|
-
lastApi: {
|
|
1693
|
-
contextTokens: usage.lastContextTokens,
|
|
1694
|
-
inputTokens: freshInput,
|
|
1695
|
-
rawInputTokens: usage.lastInputTokens,
|
|
1696
|
-
outputTokens: usage.lastOutputTokens,
|
|
1697
|
-
},
|
|
1698
|
-
cache: {
|
|
1699
|
-
hitRate: cacheHitRate,
|
|
1700
|
-
readTokens: usage.lastCachedReadTokens,
|
|
1701
|
-
writeTokens: cacheWrite,
|
|
1702
|
-
},
|
|
1703
|
-
extensions: {
|
|
1704
|
-
skills: skills.count,
|
|
1705
|
-
plugins: plugins.count,
|
|
1706
|
-
},
|
|
1707
|
-
mcp: {
|
|
1708
|
-
connected: mcp.connectedCount,
|
|
1709
|
-
configured: mcp.configuredCount,
|
|
1710
|
-
failed: mcp.failedCount,
|
|
1711
|
-
tools: tools.mcpToolCount,
|
|
1712
|
-
activeTools: tools.activeMcpToolCount,
|
|
1713
|
-
schemaTokens: mcpToolSchemaTokens,
|
|
1714
|
-
},
|
|
1715
|
-
},
|
|
1716
|
-
rows: contextRows,
|
|
1717
|
-
});
|
|
1718
|
-
};
|
|
874
|
+
// Usage-quota dashboard + /context breakdown panels:
|
|
875
|
+
// app/usage-context-panels.mjs (same factory pattern as the pickers).
|
|
876
|
+
const { openUsagePanel, openContextPicker } = createUsageContextPanels({
|
|
877
|
+
store,
|
|
878
|
+
state,
|
|
879
|
+
usageRequestRef,
|
|
880
|
+
setPicker,
|
|
881
|
+
setProviderPrompt,
|
|
882
|
+
setChannelPrompt,
|
|
883
|
+
setHookPrompt,
|
|
884
|
+
setSettingsPrompt,
|
|
885
|
+
setContextPanel,
|
|
886
|
+
setUsagePanel,
|
|
887
|
+
closeUsagePanel,
|
|
888
|
+
});
|
|
1719
889
|
|
|
1720
890
|
useEffect(() => {
|
|
1721
891
|
if (contextPanel?.kind === 'context') {
|
|
@@ -1759,446 +929,40 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1759
929
|
};
|
|
1760
930
|
}, [store, forceOnboarding]);
|
|
1761
931
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
setProviderPrompt(null);
|
|
1797
|
-
if (afterSave) afterSave();
|
|
1798
|
-
else void openProviderSetupPicker();
|
|
1799
|
-
return true;
|
|
1800
|
-
} catch (e) {
|
|
1801
|
-
store.pushNotice(`OpenAI usage auth save failed: ${e?.message || e}`, 'error');
|
|
1802
|
-
return false;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
if (providerPrompt.kind === 'local-url') {
|
|
1806
|
-
try {
|
|
1807
|
-
store.setLocalProvider(providerPrompt.providerId, {
|
|
1808
|
-
enabled: true,
|
|
1809
|
-
baseURL: commandText || providerPrompt.defaultURL,
|
|
1810
|
-
});
|
|
1811
|
-
clearModelCaches('all');
|
|
1812
|
-
const afterSave = providerPrompt.afterSave;
|
|
1813
|
-
setProviderPrompt(null);
|
|
1814
|
-
if (afterSave) afterSave();
|
|
1815
|
-
else void openProviderSetupPicker();
|
|
1816
|
-
return true;
|
|
1817
|
-
} catch (e) {
|
|
1818
|
-
store.pushNotice(`local provider update failed: ${e?.message || e}`, 'error');
|
|
1819
|
-
return false;
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
if (providerPrompt.kind === 'oauth-code') {
|
|
1823
|
-
if (!commandText) {
|
|
1824
|
-
store.pushNotice('OAuth code is required', 'warn');
|
|
1825
|
-
return false;
|
|
1826
|
-
}
|
|
1827
|
-
if (oauthSubmitRef.current || providerPrompt.submitting) {
|
|
1828
|
-
store.pushNotice('OAuth code is already being submitted', 'warn');
|
|
1829
|
-
return false;
|
|
1830
|
-
}
|
|
1831
|
-
oauthSubmitRef.current = true;
|
|
1832
|
-
setProviderPrompt((prompt) => prompt === providerPrompt ? { ...prompt, submitting: true } : prompt);
|
|
1833
|
-
void providerPrompt.login?.completeCode(commandText)
|
|
1834
|
-
.then(() => {
|
|
1835
|
-
const successReturn = providerPrompt.successReturn;
|
|
1836
|
-
const afterSave = providerPrompt.afterSave;
|
|
1837
|
-
oauthSubmitRef.current = false;
|
|
1838
|
-
clearModelCaches('all');
|
|
1839
|
-
setProviderPrompt(null);
|
|
1840
|
-
store.pushNotice(`${providerPrompt.providerName || 'OAuth'} login complete`, 'info');
|
|
1841
|
-
if (successReturn) successReturn();
|
|
1842
|
-
else if (afterSave) afterSave();
|
|
1843
|
-
else void openProviderSetupPicker();
|
|
1844
|
-
})
|
|
1845
|
-
.catch((e) => {
|
|
1846
|
-
oauthSubmitRef.current = false;
|
|
1847
|
-
store.pushNotice(`oauth code failed: ${e?.message || e}`, 'error');
|
|
1848
|
-
setProviderPrompt(null);
|
|
1849
|
-
providerPrompt.failureReturn?.(e);
|
|
1850
|
-
});
|
|
1851
|
-
return true;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
if (channelPrompt) {
|
|
1855
|
-
if (state.commandBusy) {
|
|
1856
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
1857
|
-
return false;
|
|
1858
|
-
}
|
|
1859
|
-
try {
|
|
1860
|
-
const resumeAfterChannelPrompt = (prompt) => {
|
|
1861
|
-
const afterSave = prompt?.afterSave;
|
|
1862
|
-
setChannelPrompt(null);
|
|
1863
|
-
if (typeof afterSave === 'function') afterSave();
|
|
1864
|
-
else void openChannelSetupPicker('all');
|
|
1865
|
-
};
|
|
1866
|
-
if (channelPrompt.kind === 'discord-token') {
|
|
1867
|
-
if (!commandText) return false;
|
|
1868
|
-
store.saveDiscordToken(commandText);
|
|
1869
|
-
resumeAfterChannelPrompt(channelPrompt);
|
|
1870
|
-
return true;
|
|
1871
|
-
}
|
|
1872
|
-
if (channelPrompt.kind === 'telegram-token') {
|
|
1873
|
-
if (!commandText) return false;
|
|
1874
|
-
store.saveTelegramToken(commandText);
|
|
1875
|
-
resumeAfterChannelPrompt(channelPrompt);
|
|
1876
|
-
return true;
|
|
1877
|
-
}
|
|
1878
|
-
const parts = commandText.split('|').map((part) => part.trim());
|
|
1879
|
-
if (channelPrompt.kind === 'channel-add') {
|
|
1880
|
-
// Single-channel: the UI asks only for the channel id. Legacy
|
|
1881
|
-
// `name | id | ...` pipe input still parses (the id is the second
|
|
1882
|
-
// field) so old muscle memory does not break.
|
|
1883
|
-
const isPipe = parts.length > 1;
|
|
1884
|
-
const channelId = isPipe ? parts[1] : parts[0];
|
|
1885
|
-
Promise.resolve(store.setChannel({
|
|
1886
|
-
channelId,
|
|
1887
|
-
backend: channelPrompt.backend,
|
|
1888
|
-
}))
|
|
1889
|
-
.then(() => resumeAfterChannelPrompt(channelPrompt))
|
|
1890
|
-
.catch((e) => {
|
|
1891
|
-
store.pushNotice(`channel save failed: ${e?.message || e}`, 'error');
|
|
1892
|
-
});
|
|
1893
|
-
return true;
|
|
1894
|
-
}
|
|
1895
|
-
// schedule-add / webhook-add prompt kinds retired: schedules and
|
|
1896
|
-
// webhooks are managed in the desktop app (user decision).
|
|
1897
|
-
} catch (e) {
|
|
1898
|
-
store.pushNotice(`channels update failed: ${e?.message || e}`, 'error');
|
|
1899
|
-
return false;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
if (hookPrompt) {
|
|
1903
|
-
if (state.commandBusy) {
|
|
1904
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
1905
|
-
return false;
|
|
1906
|
-
}
|
|
1907
|
-
try {
|
|
1908
|
-
if (hookPrompt.kind === 'rule-add') {
|
|
1909
|
-
const parsed = parseHookRuleInput(commandText);
|
|
1910
|
-
if (parsed.error) {
|
|
1911
|
-
store.pushNotice(parsed.error, 'warn');
|
|
1912
|
-
return false;
|
|
1913
|
-
}
|
|
1914
|
-
store.addHookRule?.(parsed.rule);
|
|
1915
|
-
setHookPrompt(null);
|
|
1916
|
-
void openHooksPicker();
|
|
1917
|
-
return true;
|
|
1918
|
-
}
|
|
1919
|
-
} catch (e) {
|
|
1920
|
-
store.pushNotice(`hook update failed: ${e?.message || e}`, 'error');
|
|
1921
|
-
return false;
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
if (settingsPrompt) {
|
|
1925
|
-
if (state.commandBusy) {
|
|
1926
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
1927
|
-
return false;
|
|
1928
|
-
}
|
|
1929
|
-
try {
|
|
1930
|
-
if (settingsPrompt.kind === 'cwd') {
|
|
1931
|
-
if (!commandText) {
|
|
1932
|
-
store.pushNotice('working directory path is required', 'warn');
|
|
1933
|
-
return false;
|
|
1934
|
-
}
|
|
1935
|
-
store.setCwd?.(commandText, { message: `Project set: ${projectNameFromPath(commandText)}` });
|
|
1936
|
-
setSettingsPrompt(null);
|
|
1937
|
-
void openSettingsPicker();
|
|
1938
|
-
return true;
|
|
1939
|
-
}
|
|
1940
|
-
if (settingsPrompt.kind === 'project-new') {
|
|
1941
|
-
if (!commandText) {
|
|
1942
|
-
store.pushNotice('project path is required', 'warn');
|
|
1943
|
-
return false;
|
|
1944
|
-
}
|
|
1945
|
-
const path = resolveProjectPath(commandText);
|
|
1946
|
-
if (isDirectory(path)) {
|
|
1947
|
-
setSettingsPrompt(null);
|
|
1948
|
-
registerProject(path);
|
|
1949
|
-
return true;
|
|
1950
|
-
}
|
|
1951
|
-
// A path that exists but is a regular file is not a valid project dir.
|
|
1952
|
-
if (pathExists(path)) {
|
|
1953
|
-
store.pushNotice(`${path} is not a directory`, 'warn');
|
|
1954
|
-
return false;
|
|
1955
|
-
}
|
|
1956
|
-
// Missing folder: confirm creation before registering.
|
|
1957
|
-
setSettingsPrompt({
|
|
1958
|
-
kind: 'project-create-confirm',
|
|
1959
|
-
label: 'New project · Create folder?',
|
|
1960
|
-
hint: `${path} does not exist. Type "y" to create it, or anything else to cancel.`,
|
|
1961
|
-
pendingPath: path,
|
|
1962
|
-
});
|
|
1963
|
-
return true;
|
|
1964
|
-
}
|
|
1965
|
-
if (settingsPrompt.kind === 'project-create-confirm') {
|
|
1966
|
-
const pendingPath = String(settingsPrompt.pendingPath || '');
|
|
1967
|
-
const answer = String(commandText || '').trim().toLowerCase();
|
|
1968
|
-
if (answer === 'y' || answer === 'yes') {
|
|
1969
|
-
const created = ensureDir(pendingPath);
|
|
1970
|
-
if (!created) {
|
|
1971
|
-
store.pushNotice(`could not create folder: ${pendingPath}`, 'error');
|
|
1972
|
-
setSettingsPrompt(null);
|
|
1973
|
-
return true;
|
|
1974
|
-
}
|
|
1975
|
-
setSettingsPrompt(null);
|
|
1976
|
-
registerProject(pendingPath);
|
|
1977
|
-
return true;
|
|
1978
|
-
}
|
|
1979
|
-
setSettingsPrompt(null);
|
|
1980
|
-
store.pushNotice('project creation canceled', 'info');
|
|
1981
|
-
return true;
|
|
1982
|
-
}
|
|
1983
|
-
if (settingsPrompt.kind === 'project-rename') {
|
|
1984
|
-
const targetPath = String(settingsPrompt.projectPath || '');
|
|
1985
|
-
try {
|
|
1986
|
-
const updated = renameProject(targetPath, commandText);
|
|
1987
|
-
if (updated) {
|
|
1988
|
-
store.pushNotice(`project renamed to "${updated.name}"`, 'info');
|
|
1989
|
-
}
|
|
1990
|
-
} catch (e) {
|
|
1991
|
-
store.pushNotice(`rename failed: ${e?.message || e}`, 'error');
|
|
1992
|
-
}
|
|
1993
|
-
setSettingsPrompt(null);
|
|
1994
|
-
openProjectPicker();
|
|
1995
|
-
return true;
|
|
1996
|
-
}
|
|
1997
|
-
if (settingsPrompt.kind === 'system-shell') {
|
|
1998
|
-
store.setSystemShell?.(commandText);
|
|
1999
|
-
setSettingsPrompt(null);
|
|
2000
|
-
void openSettingsPicker();
|
|
2001
|
-
return true;
|
|
2002
|
-
}
|
|
2003
|
-
if (settingsPrompt.kind === 'autoclear-provider') {
|
|
2004
|
-
const provider = String(settingsPrompt.provider || '').trim();
|
|
2005
|
-
if (!provider) {
|
|
2006
|
-
store.pushNotice('auto-clear provider is missing', 'warn');
|
|
2007
|
-
return false;
|
|
2008
|
-
}
|
|
2009
|
-
const text = String(commandText || '').trim();
|
|
2010
|
-
try {
|
|
2011
|
-
if (text) store.setAutoClear?.({ provider, duration: text });
|
|
2012
|
-
else store.setAutoClear?.({ provider, resetProvider: true });
|
|
2013
|
-
store.pushNotice(text ? `Auto-clear ${provider} default set to ${text}` : `Auto-clear ${provider} default reset`, 'info');
|
|
2014
|
-
} catch (e) {
|
|
2015
|
-
store.pushNotice(`autoclear failed: ${e?.message || e}`, 'error');
|
|
2016
|
-
return false;
|
|
2017
|
-
}
|
|
2018
|
-
setSettingsPrompt(null);
|
|
2019
|
-
openAutoClearPicker({ advanced: true, returnTo: settingsPrompt.returnTo });
|
|
2020
|
-
return true;
|
|
2021
|
-
}
|
|
2022
|
-
if (settingsPrompt.kind === 'profile-title') {
|
|
2023
|
-
try {
|
|
2024
|
-
store.setProfile?.({ title: commandText });
|
|
2025
|
-
store.pushNotice(commandText ? `Title set to "${commandText.trim()}"` : 'Title cleared', 'info');
|
|
2026
|
-
} catch (e) {
|
|
2027
|
-
store.pushNotice(`profile update failed: ${e?.message || e}`, 'error');
|
|
2028
|
-
}
|
|
2029
|
-
setSettingsPrompt(null);
|
|
2030
|
-
openProfilePicker();
|
|
2031
|
-
return true;
|
|
2032
|
-
}
|
|
2033
|
-
if (settingsPrompt.kind === 'plugin-add') {
|
|
2034
|
-
if (!commandText) {
|
|
2035
|
-
store.pushNotice('plugin URL/path is required', 'warn');
|
|
2036
|
-
return false;
|
|
2037
|
-
}
|
|
2038
|
-
void store.addPlugin?.(commandText)
|
|
2039
|
-
.then(() => openPluginsPicker())
|
|
2040
|
-
.catch((e) => store.pushNotice(`plugin add failed: ${e?.message || e}`, 'error'));
|
|
2041
|
-
setSettingsPrompt(null);
|
|
2042
|
-
return true;
|
|
2043
|
-
}
|
|
2044
|
-
if (settingsPrompt.kind === 'mcp-add') {
|
|
2045
|
-
const parsed = parseMcpServerInput(commandText);
|
|
2046
|
-
if (parsed.error) {
|
|
2047
|
-
store.pushNotice(parsed.error, 'warn');
|
|
2048
|
-
return false;
|
|
2049
|
-
}
|
|
2050
|
-
void store.addMcpServer?.(parsed.server)
|
|
2051
|
-
.then(() => openMcpServersPicker())
|
|
2052
|
-
.catch((e) => store.pushNotice(`mcp add failed: ${e?.message || e}`, 'error'));
|
|
2053
|
-
setSettingsPrompt(null);
|
|
2054
|
-
return true;
|
|
2055
|
-
}
|
|
2056
|
-
if (settingsPrompt.kind === 'skill-add') {
|
|
2057
|
-
const parsed = parseSkillInput(commandText);
|
|
2058
|
-
if (parsed.error) {
|
|
2059
|
-
store.pushNotice(parsed.error, 'warn');
|
|
2060
|
-
return false;
|
|
2061
|
-
}
|
|
2062
|
-
void store.addSkill?.(parsed.skill)
|
|
2063
|
-
.then(() => openProjectSkillsPicker())
|
|
2064
|
-
.catch((e) => store.pushNotice(`skill add failed: ${e?.message || e}`, 'error'));
|
|
2065
|
-
setSettingsPrompt(null);
|
|
2066
|
-
return true;
|
|
2067
|
-
}
|
|
2068
|
-
if (settingsPrompt.kind === 'skill-use') {
|
|
2069
|
-
const skillName = String(settingsPrompt.skillName || '').trim();
|
|
2070
|
-
if (!skillName) {
|
|
2071
|
-
store.pushNotice('skill name is missing', 'warn');
|
|
2072
|
-
return false;
|
|
2073
|
-
}
|
|
2074
|
-
const prompt = `$${skillName}${commandText ? ` ${commandText}` : ''}`;
|
|
2075
|
-
setSettingsPrompt(null);
|
|
2076
|
-
const accepted = store.submit(prompt);
|
|
2077
|
-
if (accepted) armTranscriptFollow();
|
|
2078
|
-
return accepted;
|
|
2079
|
-
}
|
|
2080
|
-
if (settingsPrompt.kind === 'core-add') {
|
|
2081
|
-
const sentence = commandText.trim();
|
|
2082
|
-
if (!sentence) {
|
|
2083
|
-
store.pushNotice('memory sentence is required', 'warn');
|
|
2084
|
-
return false;
|
|
2085
|
-
}
|
|
2086
|
-
setSettingsPrompt(null);
|
|
2087
|
-
void store.memoryControl?.({ action: 'core', op: 'add', project_id: 'common', element: sentence, summary: sentence }, { silent: true })
|
|
2088
|
-
.then((result) => {
|
|
2089
|
-
const errText = memoryCoreResultErrorText(result);
|
|
2090
|
-
store.pushNotice(errText || 'core memory added', errText ? 'error' : 'info');
|
|
2091
|
-
openMemoryCorePicker();
|
|
2092
|
-
})
|
|
2093
|
-
.catch((e) => {
|
|
2094
|
-
store.pushNotice(`core add failed: ${e?.message || e}`, 'error');
|
|
2095
|
-
openMemoryCorePicker();
|
|
2096
|
-
});
|
|
2097
|
-
return true;
|
|
2098
|
-
}
|
|
2099
|
-
if (settingsPrompt.kind === 'core-edit') {
|
|
2100
|
-
const sentence = commandText.trim();
|
|
2101
|
-
const id = settingsPrompt._id;
|
|
2102
|
-
const projectId = settingsPrompt._projectId ?? 'common';
|
|
2103
|
-
if (!sentence) {
|
|
2104
|
-
store.pushNotice('memory sentence is required', 'warn');
|
|
2105
|
-
return false;
|
|
2106
|
-
}
|
|
2107
|
-
setSettingsPrompt(null);
|
|
2108
|
-
// Single-sentence semantics only rewrite `element` when the row was
|
|
2109
|
-
// already element===summary at load (see beginEditCoreMemory's
|
|
2110
|
-
// _singleSentence flag). A distinct legacy element carries meaning
|
|
2111
|
-
// this text prompt never captured -- clobbering it on every edit
|
|
2112
|
-
// would corrupt the entry (and re-embed/dedupe on the clobbered
|
|
2113
|
-
// value). Otherwise only `summary` is sent.
|
|
2114
|
-
const editArgs = settingsPrompt._singleSentence
|
|
2115
|
-
? { action: 'core', op: 'edit', id, project_id: projectId, element: sentence, summary: sentence }
|
|
2116
|
-
: { action: 'core', op: 'edit', id, project_id: projectId, summary: sentence };
|
|
2117
|
-
void store.memoryControl?.(editArgs, { silent: true })
|
|
2118
|
-
.then((result) => {
|
|
2119
|
-
const errText = memoryCoreResultErrorText(result);
|
|
2120
|
-
store.pushNotice(errText || 'core memory updated', errText ? 'error' : 'info');
|
|
2121
|
-
openMemoryCorePicker();
|
|
2122
|
-
})
|
|
2123
|
-
.catch((e) => {
|
|
2124
|
-
store.pushNotice(`core edit failed: ${e?.message || e}`, 'error');
|
|
2125
|
-
openMemoryCorePicker();
|
|
2126
|
-
});
|
|
2127
|
-
return true;
|
|
2128
|
-
}
|
|
2129
|
-
if (settingsPrompt.kind === 'core-delete-confirm') {
|
|
2130
|
-
const id = settingsPrompt._id;
|
|
2131
|
-
const projectId = settingsPrompt._projectId ?? 'common';
|
|
2132
|
-
const answer = String(commandText || '').trim().toLowerCase();
|
|
2133
|
-
setSettingsPrompt(null);
|
|
2134
|
-
if (answer !== 'y' && answer !== 'yes') {
|
|
2135
|
-
store.pushNotice('delete canceled', 'info');
|
|
2136
|
-
openMemoryCorePicker();
|
|
2137
|
-
return true;
|
|
2138
|
-
}
|
|
2139
|
-
void store.memoryControl?.({ action: 'core', op: 'delete', id, project_id: projectId }, { silent: true })
|
|
2140
|
-
.then((result) => {
|
|
2141
|
-
const errText = memoryCoreResultErrorText(result);
|
|
2142
|
-
store.pushNotice(errText || 'core memory deleted', errText ? 'error' : 'info');
|
|
2143
|
-
openMemoryCorePicker();
|
|
2144
|
-
})
|
|
2145
|
-
.catch((e) => {
|
|
2146
|
-
store.pushNotice(`core delete failed: ${e?.message || e}`, 'error');
|
|
2147
|
-
openMemoryCorePicker();
|
|
2148
|
-
});
|
|
2149
|
-
return true;
|
|
2150
|
-
}
|
|
2151
|
-
} catch (e) {
|
|
2152
|
-
store.pushNotice(`settings update failed: ${e?.message || e}`, 'error');
|
|
2153
|
-
return false;
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
if (!commandText) return false;
|
|
2157
|
-
|
|
2158
|
-
if (commandText.startsWith('/')) {
|
|
2159
|
-
if (state.commandBusy) {
|
|
2160
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
2161
|
-
return false;
|
|
2162
|
-
}
|
|
2163
|
-
const [cmd, ...rest] = commandText.slice(1).split(/\s+/);
|
|
2164
|
-
const accepted = runSlashCommand(cmd, rest.join(' ').trim());
|
|
2165
|
-
if (accepted !== false) clearPastedImagesSnapshot();
|
|
2166
|
-
return accepted;
|
|
2167
|
-
}
|
|
2168
|
-
const imageRefs = imageReferenceIds(text);
|
|
2169
|
-
const imageSnapshot = Object.fromEntries(Object.entries(pastedImagesRef.current || {})
|
|
2170
|
-
.filter(([id]) => imageRefs.has(Number(id))));
|
|
2171
|
-
const hasImageSnapshot = Object.keys(imageSnapshot).length > 0;
|
|
2172
|
-
// Expand folded [Pasted text #N +M lines] tokens back to their original
|
|
2173
|
-
// text at the same point buildPromptContentWithImages runs. Broken /
|
|
2174
|
-
// partially-deleted tokens do not match and are left as-is.
|
|
2175
|
-
const textRefs = pastedTextReferenceIds(text);
|
|
2176
|
-
const textSnapshot = Object.fromEntries(Object.entries(pastedTextsRef.current || {})
|
|
2177
|
-
.filter(([id]) => textRefs.has(Number(id))));
|
|
2178
|
-
const hasTextSnapshot = Object.keys(textSnapshot).length > 0;
|
|
2179
|
-
const expandedText = hasTextSnapshot ? expandPastedTextTokens(text, textSnapshot) : text;
|
|
2180
|
-
const content = buildPromptContentWithImages(expandedText, imageSnapshot);
|
|
2181
|
-
const accepted = store.submit(content, {
|
|
2182
|
-
// Store the EXPANDED text in the transcript/history so a later prompt-
|
|
2183
|
-
// history recall resubmits the real content, not the literal token
|
|
2184
|
-
// (pastedTexts entries are cleared on accept). History recall therefore
|
|
2185
|
-
// shows the full original text instead of the token — acceptable.
|
|
2186
|
-
displayText: expandedText,
|
|
2187
|
-
pastedImages: imageSnapshot,
|
|
2188
|
-
pastedTexts: textSnapshot,
|
|
2189
|
-
onCommitted: (hasImageSnapshot || hasTextSnapshot)
|
|
2190
|
-
? () => { clearPastedImagesSnapshot(imageSnapshot); clearPastedTextsSnapshot(textSnapshot); }
|
|
2191
|
-
: null,
|
|
2192
|
-
});
|
|
2193
|
-
if (accepted) {
|
|
2194
|
-
armTranscriptFollow();
|
|
2195
|
-
if (imageRefs.size === 0 || (!hasImageSnapshot && !state.busy)) clearPastedImagesSnapshot();
|
|
2196
|
-
else if (state.busy && hasImageSnapshot) clearPastedImagesSnapshot(imageSnapshot);
|
|
2197
|
-
if (textRefs.size === 0 || (!hasTextSnapshot && !state.busy)) clearPastedTextsSnapshot();
|
|
2198
|
-
else if (state.busy && hasTextSnapshot) clearPastedTextsSnapshot(textSnapshot);
|
|
2199
|
-
}
|
|
2200
|
-
return accepted;
|
|
2201
|
-
};
|
|
932
|
+
// Prompt submit dispatcher (text-entry prompts, slash commands, chat
|
|
933
|
+
// submit + pasted-token expansion): app/prompt-submit.mjs.
|
|
934
|
+
const { onSubmit } = createPromptSubmit({
|
|
935
|
+
store,
|
|
936
|
+
state,
|
|
937
|
+
providerPrompt,
|
|
938
|
+
channelPrompt,
|
|
939
|
+
hookPrompt,
|
|
940
|
+
settingsPrompt,
|
|
941
|
+
setProviderPrompt,
|
|
942
|
+
setChannelPrompt,
|
|
943
|
+
setHookPrompt,
|
|
944
|
+
setSettingsPrompt,
|
|
945
|
+
oauthSubmitRef,
|
|
946
|
+
clearModelCaches,
|
|
947
|
+
openProviderSetupPicker,
|
|
948
|
+
openChannelSetupPicker,
|
|
949
|
+
openHooksPicker,
|
|
950
|
+
openSettingsPicker,
|
|
951
|
+
openProjectPicker,
|
|
952
|
+
openAutoClearPicker,
|
|
953
|
+
openProfilePicker,
|
|
954
|
+
openPluginsPicker,
|
|
955
|
+
openMcpServersPicker,
|
|
956
|
+
openProjectSkillsPicker,
|
|
957
|
+
openMemoryCorePicker,
|
|
958
|
+
registerProject,
|
|
959
|
+
runSlashCommand,
|
|
960
|
+
armTranscriptFollow,
|
|
961
|
+
clearPastedImagesSnapshot,
|
|
962
|
+
clearPastedTextsSnapshot,
|
|
963
|
+
pastedImagesRef,
|
|
964
|
+
pastedTextsRef,
|
|
965
|
+
});
|
|
2202
966
|
|
|
2203
967
|
const activeSlashQuery = providerPrompt || channelPrompt || hookPrompt || settingsPrompt || toolApproval || contextPanel || usagePanel ? null : slashQuery(promptDraft);
|
|
2204
968
|
// "Slash mode" is live whenever a /token is being edited and no other
|
|
@@ -2230,676 +994,113 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2230
994
|
setSlashIndex((index) => Math.min(index, Math.max(0, slashCommands.length - 1)));
|
|
2231
995
|
}, [slashCommands.length, activeSlashQuery]);
|
|
2232
996
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
setSlashDismissedFor((dismissed) => (dismissed && dismissed !== value ? '' : dismissed));
|
|
2277
|
-
}
|
|
2278
|
-
}, [clearPromptHint, dismissWelcomePromptHint, resetPromptHistoryNav, showPromptHint, slashDismissedFor, syncPromptLayoutRows]);
|
|
2279
|
-
|
|
2280
|
-
const cancelProviderPrompt = useCallback(() => {
|
|
2281
|
-
try { providerPrompt?.login?.cancel?.(); } catch {}
|
|
2282
|
-
oauthSubmitRef.current = false;
|
|
2283
|
-
const onCancel = providerPrompt?.cancelReturn || providerPrompt?.onCancel;
|
|
2284
|
-
const afterSave = providerPrompt?.afterSave;
|
|
2285
|
-
setProviderPrompt(null);
|
|
2286
|
-
if (onCancel) onCancel();
|
|
2287
|
-
else if (afterSave) afterSave();
|
|
2288
|
-
}, [providerPrompt, showPromptHint]);
|
|
2289
|
-
|
|
2290
|
-
const cancelChannelPrompt = useCallback(() => {
|
|
2291
|
-
const onCancel = channelPrompt?.onCancel;
|
|
2292
|
-
const afterSave = channelPrompt?.afterSave;
|
|
2293
|
-
setChannelPrompt(null);
|
|
2294
|
-
if (typeof onCancel === 'function') onCancel();
|
|
2295
|
-
else if (typeof afterSave === 'function') afterSave();
|
|
2296
|
-
}, [channelPrompt, showPromptHint]);
|
|
2297
|
-
|
|
2298
|
-
const cancelHookPrompt = useCallback(() => {
|
|
2299
|
-
setHookPrompt(null);
|
|
2300
|
-
}, [showPromptHint]);
|
|
2301
|
-
|
|
2302
|
-
const cancelSettingsPrompt = useCallback(() => {
|
|
2303
|
-
// The project entry prompts are reached from the project picker; backing out
|
|
2304
|
-
// (Esc) should return to that picker rather than dropping to a bare prompt.
|
|
2305
|
-
const kind = settingsPrompt?.kind;
|
|
2306
|
-
setSettingsPrompt(null);
|
|
2307
|
-
if (kind === 'project-new' || kind === 'project-create-confirm' || kind === 'project-rename') {
|
|
2308
|
-
openProjectPicker();
|
|
2309
|
-
} else if (kind === 'core-add' || kind === 'core-edit' || kind === 'core-delete-confirm') {
|
|
2310
|
-
openMemoryCorePicker();
|
|
2311
|
-
} else if (kind === 'autoclear-provider') {
|
|
2312
|
-
openAutoClearPicker({ advanced: true, returnTo: settingsPrompt?.returnTo });
|
|
2313
|
-
}
|
|
2314
|
-
}, [settingsPrompt, showPromptHint]);
|
|
2315
|
-
|
|
2316
|
-
const acceptSlashPalette = useCallback((draftValue = '') => {
|
|
2317
|
-
const command = slashCommands[slashIndex];
|
|
2318
|
-
if (!command) return false;
|
|
2319
|
-
pickerOpenedFromEnterRef.current = true;
|
|
2320
|
-
if (pickerOpenedFromEnterTimerRef.current) {
|
|
2321
|
-
clearTimeout(pickerOpenedFromEnterTimerRef.current);
|
|
2322
|
-
pickerOpenedFromEnterTimerRef.current = null;
|
|
2323
|
-
}
|
|
2324
|
-
try {
|
|
2325
|
-
return runSlashCommand(slashCommandTokenForPaletteAccept(command, draftValue), '');
|
|
2326
|
-
} finally {
|
|
2327
|
-
pickerOpenedFromEnterTimerRef.current = setTimeout(() => {
|
|
2328
|
-
pickerOpenedFromEnterRef.current = false;
|
|
2329
|
-
pickerOpenedFromEnterTimerRef.current = null;
|
|
2330
|
-
}, 3000);
|
|
2331
|
-
}
|
|
2332
|
-
}, [slashCommands, slashIndex]);
|
|
2333
|
-
|
|
2334
|
-
const completeSlashPalette = useCallback((draftValue = '') => {
|
|
2335
|
-
const command = slashCommands[slashIndex];
|
|
2336
|
-
if (!command) return undefined;
|
|
2337
|
-
const token = slashCommandTokenForPaletteAccept(command, draftValue);
|
|
2338
|
-
return token ? `/${token} ` : undefined;
|
|
2339
|
-
}, [slashCommands, slashIndex]);
|
|
2340
|
-
|
|
2341
|
-
const cancelSlashPalette = useCallback((value = '') => {
|
|
2342
|
-
// Esc clears the slash draft, so the dismissal marker must not survive.
|
|
2343
|
-
// If it stays as "/" then typing "/" again is treated as the same
|
|
2344
|
-
// dismissed query and the palette never re-opens.
|
|
2345
|
-
setSlashDismissedFor('');
|
|
2346
|
-
setPromptDraft('');
|
|
2347
|
-
setPromptDraftOverride({ id: Date.now(), value: '' });
|
|
2348
|
-
}, []);
|
|
997
|
+
// Draft/slash flow (draft sync, prompt cancels, palette accept/cancel):
|
|
998
|
+
// app/use-prompt-draft-flow.mjs.
|
|
999
|
+
const {
|
|
1000
|
+
onPromptDraftChange,
|
|
1001
|
+
cancelProviderPrompt,
|
|
1002
|
+
cancelChannelPrompt,
|
|
1003
|
+
cancelHookPrompt,
|
|
1004
|
+
cancelSettingsPrompt,
|
|
1005
|
+
acceptSlashPalette,
|
|
1006
|
+
completeSlashPalette,
|
|
1007
|
+
cancelSlashPalette,
|
|
1008
|
+
} = usePromptDraftFlow({
|
|
1009
|
+
dismissWelcomePromptHint,
|
|
1010
|
+
syncPromptLayoutRows,
|
|
1011
|
+
promptHistoryDraftChangeRef,
|
|
1012
|
+
promptHistoryNavRef,
|
|
1013
|
+
resetPromptHistoryNav,
|
|
1014
|
+
setPromptDraft,
|
|
1015
|
+
setPromptDraftOverride,
|
|
1016
|
+
showPromptHint,
|
|
1017
|
+
clearPromptHint,
|
|
1018
|
+
promptHintActiveRef,
|
|
1019
|
+
promptHintTimerRef,
|
|
1020
|
+
slashDismissedFor,
|
|
1021
|
+
setSlashDismissedFor,
|
|
1022
|
+
providerPrompt,
|
|
1023
|
+
channelPrompt,
|
|
1024
|
+
hookPrompt,
|
|
1025
|
+
settingsPrompt,
|
|
1026
|
+
setProviderPrompt,
|
|
1027
|
+
setChannelPrompt,
|
|
1028
|
+
setHookPrompt,
|
|
1029
|
+
setSettingsPrompt,
|
|
1030
|
+
oauthSubmitRef,
|
|
1031
|
+
openProjectPicker,
|
|
1032
|
+
openMemoryCorePicker,
|
|
1033
|
+
openAutoClearPicker,
|
|
1034
|
+
slashCommands,
|
|
1035
|
+
slashIndex,
|
|
1036
|
+
pickerOpenedFromEnterRef,
|
|
1037
|
+
pickerOpenedFromEnterTimerRef,
|
|
1038
|
+
runSlashCommand,
|
|
1039
|
+
});
|
|
2349
1040
|
|
|
2350
1041
|
const resizeEpoch = resizeState.epoch;
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
|
|
2354
|
-
// recomputes when agent state actually changes, not on every assistant delta.
|
|
2355
|
-
const agentRevision = useMemo(() => JSON.stringify({
|
|
2356
|
-
workers: (state.agentWorkers || []).map((w) => [w.tag, w.status, w.stage, w.sessionId]).slice(0, 20),
|
|
2357
|
-
jobs: (state.agentJobs || []).map((j) => [j.task_id, j.status, j.tag, j.sessionId, j.startedAt, j.finishedAt, j.error]).slice(0, 20),
|
|
2358
|
-
}), [state.agentWorkers, state.agentJobs]);
|
|
1042
|
+
// Agent revision + active-tool signature + statusline stats:
|
|
1043
|
+
// app/use-transcript-activity.mjs.
|
|
1044
|
+
const { agentRevision, activeToolsSignature, activeTools, statuslineStats } = useTranscriptActivity({ state });
|
|
2359
1045
|
|
|
2360
|
-
//
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
// the StatusLine effect only re-fires when the numbers actually change.
|
|
2373
|
-
const activeToolsSignature = useMemo(() => {
|
|
2374
|
-
// The engine maintains this signature incrementally (updated on tool
|
|
2375
|
-
// start/early-complete/result/turn-end), so App no longer scans every
|
|
2376
|
-
// transcript item on each change. Prefer it; fall back to the local scan
|
|
2377
|
-
// only when the engine did not publish it (older snapshot).
|
|
2378
|
-
if (state.activeToolSummary !== undefined) return state.activeToolSummary || '';
|
|
2379
|
-
const items = state.items || [];
|
|
2380
|
-
let exploreCount = 0;
|
|
2381
|
-
let exploreStart = 0;
|
|
2382
|
-
let searchCount = 0;
|
|
2383
|
-
let searchStart = 0;
|
|
2384
|
-
for (const it of items) {
|
|
2385
|
-
if (!it || it.kind !== 'tool') continue;
|
|
2386
|
-
const count = Math.max(1, Number(it.count || 1));
|
|
2387
|
-
// Resolved check: aggregates stay on the pure completedCount>=count test
|
|
2388
|
-
// because engine patchToolCardResult sets `result` on EVERY aggregate
|
|
2389
|
-
// patch (even partial, completedCount<count), so a `result`-aware check
|
|
2390
|
-
// would drop a still-running aggregate early. Standalone cards mirror
|
|
2391
|
-
// toolItemPendingForRows (done when completedCount>=count OR a result
|
|
2392
|
-
// landed) so an abnormally-finished card (cancelled/errored) that sets a
|
|
2393
|
-
// result without bumping completedCount cannot pin a phantom segment.
|
|
2394
|
-
const done = it.aggregate
|
|
2395
|
-
? Number(it.completedCount || 0)
|
|
2396
|
-
: Math.max(0, Math.min(count, Number(it.completedCount || (it.result == null ? 0 : count))));
|
|
2397
|
-
if (done >= count) continue; // resolved card (matches toolItemPendingForRows)
|
|
2398
|
-
const started = Number(it.startedAt || 0);
|
|
2399
|
-
let exploreHits = 0;
|
|
2400
|
-
let searchHits = 0;
|
|
2401
|
-
if (it.aggregate && it.categories && typeof it.categories === 'object') {
|
|
2402
|
-
for (const v of Object.values(it.categories)) {
|
|
2403
|
-
const cat = v && typeof v === 'object' ? v.category : null;
|
|
2404
|
-
const c = Math.max(1, Number(v && typeof v === 'object' ? v.count : 1) || 1);
|
|
2405
|
-
if (cat === 'Explore') exploreHits += c;
|
|
2406
|
-
else if (cat === 'Web Research') searchHits += c;
|
|
2407
|
-
}
|
|
2408
|
-
} else if (it.name) {
|
|
2409
|
-
const cat = classifyToolCategory(it.name, it.args || {});
|
|
2410
|
-
if (cat === 'Explore') exploreHits = count;
|
|
2411
|
-
else if (cat === 'Web Research') searchHits = count;
|
|
2412
|
-
}
|
|
2413
|
-
if (exploreHits > 0) {
|
|
2414
|
-
exploreCount += exploreHits;
|
|
2415
|
-
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
2416
|
-
}
|
|
2417
|
-
if (searchHits > 0) {
|
|
2418
|
-
searchCount += searchHits;
|
|
2419
|
-
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
if (!exploreCount && !searchCount) return '';
|
|
2423
|
-
return `${exploreCount}:${exploreStart}:${searchCount}:${searchStart}`;
|
|
2424
|
-
}, [state.activeToolSummary, state.items]);
|
|
2425
|
-
|
|
2426
|
-
const activeTools = useMemo(() => {
|
|
2427
|
-
if (!activeToolsSignature) return null;
|
|
2428
|
-
const [ec, es, sc, ss] = activeToolsSignature.split(':').map((n) => Number(n) || 0);
|
|
2429
|
-
return {
|
|
2430
|
-
explore: { count: ec, startedAt: es },
|
|
2431
|
-
search: { count: sc, startedAt: ss },
|
|
2432
|
-
};
|
|
2433
|
-
}, [activeToolsSignature]);
|
|
2434
|
-
|
|
2435
|
-
// StatusLine only reads a small stats subset; engine clones the full stats
|
|
2436
|
-
// object on many updates. Memoize by field value so identical usage keeps
|
|
2437
|
-
// the same object reference and React.memo / effect deps stay quiet.
|
|
2438
|
-
const statuslineStats = useMemo(() => {
|
|
2439
|
-
const s = state.stats || {};
|
|
2440
|
-
return {
|
|
2441
|
-
currentContextSource: s.currentContextSource ?? null,
|
|
2442
|
-
currentEstimatedContextTokens: s.currentEstimatedContextTokens ?? 0,
|
|
2443
|
-
currentContextTokens: s.currentContextTokens ?? 0,
|
|
2444
|
-
contextTokens: s.contextTokens ?? 0,
|
|
2445
|
-
latestPromptTokens: s.latestPromptTokens ?? 0,
|
|
2446
|
-
latestInputTokens: s.latestInputTokens ?? 0,
|
|
2447
|
-
latestCachedTokens: s.latestCachedTokens ?? 0,
|
|
2448
|
-
latestCacheWriteTokens: s.latestCacheWriteTokens ?? 0,
|
|
2449
|
-
inputTokens: s.inputTokens ?? 0,
|
|
2450
|
-
cachedTokens: s.cachedTokens ?? 0,
|
|
2451
|
-
cacheWriteTokens: s.cacheWriteTokens ?? 0,
|
|
2452
|
-
promptTokens: s.promptTokens ?? 0,
|
|
2453
|
-
turns: s.turns ?? 0,
|
|
2454
|
-
};
|
|
2455
|
-
}, [
|
|
2456
|
-
state.stats?.currentContextSource,
|
|
2457
|
-
state.stats?.currentEstimatedContextTokens,
|
|
2458
|
-
state.stats?.currentContextTokens,
|
|
2459
|
-
state.stats?.contextTokens,
|
|
2460
|
-
state.stats?.latestPromptTokens,
|
|
2461
|
-
state.stats?.latestInputTokens,
|
|
2462
|
-
state.stats?.latestCachedTokens,
|
|
2463
|
-
state.stats?.latestCacheWriteTokens,
|
|
2464
|
-
state.stats?.inputTokens,
|
|
2465
|
-
state.stats?.cachedTokens,
|
|
2466
|
-
state.stats?.cacheWriteTokens,
|
|
2467
|
-
state.stats?.promptTokens,
|
|
2468
|
-
state.stats?.turns,
|
|
2469
|
-
]);
|
|
2470
|
-
|
|
2471
|
-
// ── Transcript viewport height ──────────────────────────────────────────
|
|
2472
|
-
// ROOT-CAUSE FIX: the transcript must live in a box with an EXPLICIT numeric
|
|
2473
|
-
// height + overflow:hidden so ink's renderer actually clips off-screen rows
|
|
2474
|
-
// (render-node-to-output.js → output.clip uses the box's computed height). An
|
|
2475
|
-
// unbounded negative-margin column inside a flexGrow box let stale rows
|
|
2476
|
-
// overprint newer ones across incremental redraws. We reserve the rows the
|
|
2477
|
-
// bottom cluster needs and give the transcript everything above it.
|
|
2478
|
-
//
|
|
2479
|
-
// viewportHeight = rows
|
|
2480
|
-
// − welcome header (empty transcript only)
|
|
2481
|
-
// − live status (thinking / spinner / TurnDone)
|
|
2482
|
-
// − queued prompts (marginTop 1 + N rows, only when queued)
|
|
2483
|
-
// − prompt meta (spinner / transient message / queued)
|
|
2484
|
-
// − input box (2 border + wrapped content)
|
|
2485
|
-
// − statusline (reserved L1 + L2 + outer gap; total 3 rows)
|
|
2486
|
-
//
|
|
2487
|
-
// Every sibling outside the viewport must be accounted for here; otherwise
|
|
2488
|
-
// the total tree height exceeds the terminal and the input box gets pushed.
|
|
2489
|
-
const textEntryPrompt = providerPrompt || channelPrompt || hookPrompt || settingsPrompt;
|
|
2490
|
-
const hasTextEntryPrompt = !!textEntryPrompt;
|
|
2491
|
-
const hasFloatingPanel = !!(toolApproval || picker || contextPanel || usagePanel || slashPaletteOpen || hasTextEntryPrompt);
|
|
2492
|
-
const expandedOptionPanel = !!(toolApproval || picker || contextPanel || usagePanel || hasTextEntryPrompt);
|
|
2493
|
-
const panelTransitionForBoot = panelTransitionRef.current;
|
|
2494
|
-
if (panelTransitionForBoot.signature.includes('picker:project') && !picker) {
|
|
2495
|
-
projectBootInputLatchRef.current = true;
|
|
2496
|
-
}
|
|
2497
|
-
const bootSettling = !tuiReady && state.items.length === 0 && !hasFloatingPanel && !projectBootInputLatchRef.current;
|
|
2498
|
-
// Project selection (initial-entry experience) keeps the welcome banner
|
|
2499
|
-
// visible above the picker / path-entry prompt, unlike other floating panels.
|
|
2500
|
-
const projectSelectionActive = picker?.kind === 'project'
|
|
2501
|
-
|| settingsPrompt?.kind === 'project-new'
|
|
2502
|
-
|| settingsPrompt?.kind === 'project-create-confirm'
|
|
2503
|
-
|| settingsPrompt?.kind === 'project-rename';
|
|
2504
|
-
// Slash search floats above the normal prompt. Actual option panels own the
|
|
2505
|
-
// prompt/status area, so they hide those rows and expand into that space.
|
|
2506
|
-
const inputBoxHidden = expandedOptionPanel || bootSettling;
|
|
2507
|
-
const liveSpinner = state.spinner?.active ? state.spinner : (state.commandStatus?.active ? state.commandStatus : null);
|
|
2508
|
-
// Command-status spinner (auto-clear/compact/etc.) is NOT part of the
|
|
2509
|
-
// spinner → TurnDone handoff: it typically starts while the transcript tail
|
|
2510
|
-
// is already a done row (idle session), so the done-at-tail suppression
|
|
2511
|
-
// below must never hide it — that read as a frozen UI during auto-clear.
|
|
2512
|
-
const liveSpinnerIsCommand = !state.spinner?.active && !!state.commandStatus?.active;
|
|
2513
|
-
const latestToast = state.toasts?.length ? state.toasts[state.toasts.length - 1] : null;
|
|
2514
|
-
const toastHint = latestToast ? latestToast.text : '';
|
|
2515
|
-
const progressHint = state.progressHint || null;
|
|
2516
|
-
const inputHint = promptHint || toastHint || (progressHint?.text || '');
|
|
2517
|
-
const inputHintTone = promptHint
|
|
2518
|
-
? promptHintTone
|
|
2519
|
-
: (latestToast?.tone || progressHint?.tone || 'info');
|
|
2520
|
-
const latestTranscriptItem = state.items[state.items.length - 1] || null;
|
|
2521
|
-
// Bottom meta band ownership is LIVE-SPINNER ONLY. A finished turn's done row
|
|
2522
|
-
// (turndone/statusdone) is a normal transcript item and flows into scrollback
|
|
2523
|
-
// like anything else, so the area directly above the prompt is CLEAR when the
|
|
2524
|
-
// user is idle. Earlier this row was pinned in the meta band until the next
|
|
2525
|
-
// transcript item was appended (to dodge an autowrap overprint/bleed), which
|
|
2526
|
-
// left the completed status row stuck above the prompt while the user typed or
|
|
2527
|
-
// sat idle. That bleed is now fixed at the source by the tool-output width
|
|
2528
|
-
// clamp, so the pin is no longer needed. Kept as a named null const so the
|
|
2529
|
-
// downstream meta-band/hint logic collapses cleanly to the spinner-only path.
|
|
2530
|
-
const latestDoneItem = null;
|
|
2531
|
-
const SCROLL_HINT_ROWS = 0;
|
|
2532
|
-
const LIVE_STATUS_ROWS = 0;
|
|
2533
|
-
// The standalone prompt box is 2 border rows + the wrapped PromptInput body.
|
|
2534
|
-
// The one-row scroll baseline gap ABOVE the prompt is owned by
|
|
2535
|
-
// transcriptGuardRows, not the prompt box itself. That keeps the scroll
|
|
2536
|
-
// reference at "textbox + 1" while the prompt/statusline bottom stays fixed.
|
|
2537
|
-
//
|
|
2538
|
-
// This must track the prompt draft's REAL wrapped height. Reserving a constant
|
|
2539
|
-
// one-line prompt lets long/multiline input grow the bottom cluster after the
|
|
2540
|
-
// transcript viewport has already claimed those rows, which makes transcript
|
|
2541
|
-
// body text overprint the textbox or slash command window.
|
|
2542
|
-
const currentPromptLayoutRows = promptContentRows(promptLayoutValueRef.current, promptContentColumns);
|
|
2543
|
-
const promptInputRows = inputBoxHidden ? 0 : currentPromptLayoutRows;
|
|
2544
|
-
const promptBoxRows = inputBoxHidden ? 0 : 2 + promptInputRows;
|
|
2545
|
-
const STATUSLINE_ROWS = 3;
|
|
2546
|
-
// Shared panel chrome math. Every floating panel follows the same vertical
|
|
2547
|
-
// rhythm INSIDE its round border: title row, blank, description/hint row,
|
|
2548
|
-
// blank, then content. That is 4 non-content rows; the round border adds 2
|
|
2549
|
-
// more, so chrome reserves 6 rows total. Reserving the full chrome here (even
|
|
2550
|
-
// for panels that omit the description) guarantees the bordered title can
|
|
2551
|
-
// never be clipped off the top — content rows shrink first when the terminal
|
|
2552
|
-
// is short, because the floating container clips from the top (flex-end).
|
|
2553
|
-
const PANEL_MAX_VISIBLE = 8;
|
|
2554
|
-
const PANEL_CHROME_ROWS = 6;
|
|
2555
|
-
const PANEL_BASE_ROWS = PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS;
|
|
2556
|
-
const PICKER_CHROME_ROWS = PANEL_CHROME_ROWS;
|
|
2557
|
-
// TextEntryPanel content is one prompt line (chrome + 1) for single-line
|
|
2558
|
-
// prompts, or up to PANEL_MAX_VISIBLE wrapped rows for core memory add/edit.
|
|
2559
|
-
// PLUS an optional wrapped detail block (blank spacer + N wrapped rows — e.g.
|
|
2560
|
-
// the manual OAuth URL). The floating container clips from the TOP (flex-end +
|
|
2561
|
-
// overflow hidden), so under-reserving here pushed the bordered title off
|
|
2562
|
-
// the top of the panel. Width matches the panel interior: frame − 2 border
|
|
2563
|
-
// − 2 paddingX, same wrap-ansi math ink uses for wrap="wrap".
|
|
2564
|
-
const textEntryKind = String(textEntryPrompt?.kind || '');
|
|
2565
|
-
const textEntryMultiline = CORE_MULTILINE_TEXT_ENTRY_KINDS.has(textEntryKind);
|
|
2566
|
-
const textEntryDetailText = String(textEntryPrompt?.detail || '').trim();
|
|
2567
|
-
const textEntryDetailRows = textEntryDetailText
|
|
2568
|
-
? 1 + wrappedDetailRows(textEntryDetailText, Math.max(1, frameColumns - 4))
|
|
2569
|
-
: 0;
|
|
2570
|
-
const textEntryContentRows = textEntryMultiline ? textEntryLayoutRows : 1;
|
|
2571
|
-
const TEXT_ENTRY_ROWS = PANEL_CHROME_ROWS + textEntryContentRows + textEntryDetailRows;
|
|
2572
|
-
const OPTION_PANEL_EXTRA_ROWS = expandedOptionPanel ? 3 : 0;
|
|
2573
|
-
const queuedVisible = !hasFloatingPanel && !inputBoxHidden && state.queued?.length > 0;
|
|
2574
|
-
// While the slash palette is open it owns the area above the prompt, so the
|
|
2575
|
-
// live spinner/meta row is suppressed entirely — no reservation and no render.
|
|
2576
|
-
// Normalize the spinner → TurnDone handoff by making them occupy the SAME
|
|
2577
|
-
// two-row slot. Completion appends turndone and clears spinner in one commit,
|
|
2578
|
-
// so a completed row replaces the spinner slot with no transient jump. A
|
|
2579
|
-
// statusdone can be emitted mid-turn (for example after compaction), so it
|
|
2580
|
-
// must not suppress the still-active spinner.
|
|
2581
|
-
const promptMetaVisible = isLiveSpinnerMetaVisible({
|
|
2582
|
-
inputBoxHidden,
|
|
1046
|
+
// Transcript viewport + bottom-cluster row budget: app/shell-layout.mjs.
|
|
1047
|
+
const layout = computeShellLayout({
|
|
1048
|
+
providerPrompt,
|
|
1049
|
+
channelPrompt,
|
|
1050
|
+
hookPrompt,
|
|
1051
|
+
settingsPrompt,
|
|
1052
|
+
panelTransitionEpoch,
|
|
1053
|
+
panelInkMaskEpoch,
|
|
1054
|
+
toolApproval,
|
|
1055
|
+
picker,
|
|
1056
|
+
contextPanel,
|
|
1057
|
+
usagePanel,
|
|
2583
1058
|
slashPaletteOpen,
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
1059
|
+
tuiReady,
|
|
1060
|
+
state,
|
|
1061
|
+
resizeState,
|
|
1062
|
+
frameColumns,
|
|
1063
|
+
promptHint,
|
|
1064
|
+
promptHintTone,
|
|
1065
|
+
textEntryLayoutRows,
|
|
1066
|
+
onboardingActive,
|
|
1067
|
+
conditionalWelcomePromptHint,
|
|
1068
|
+
welcomePromptHintDismissed,
|
|
1069
|
+
welcomePromptHintRef,
|
|
1070
|
+
welcomePromptHintVisibleRef,
|
|
1071
|
+
panelTransitionRef,
|
|
1072
|
+
projectBootInputLatchRef,
|
|
1073
|
+
promptLayoutValueRef,
|
|
1074
|
+
promptContentColumns,
|
|
1075
|
+
transcriptBottomSlackRowsRef,
|
|
1076
|
+
transcriptViewportRef,
|
|
1077
|
+
frameRowsRef,
|
|
1078
|
+
promptBoxRectRef,
|
|
1079
|
+
panelCloseInkMaskRowsRef,
|
|
1080
|
+
CORE_MULTILINE_TEXT_ENTRY_KINDS,
|
|
1081
|
+
panelSignatureFlags,
|
|
1082
|
+
panelKindSignature,
|
|
1083
|
+
isInstantPanelCloseTransition,
|
|
1084
|
+
PANEL_LAYOUT_SIG,
|
|
2587
1085
|
});
|
|
2588
|
-
const
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
// transient hint add a visible newline/prompt jump whenever no spinner was
|
|
2592
|
-
// active.
|
|
2593
|
-
const overlayHintRequested = !inputBoxHidden && !hasFloatingPanel && !liveSpinner && !!inputHint && !queuedVisible;
|
|
2594
|
-
const overlayHintRows = 0;
|
|
2595
|
-
// QueuedCommands renders each queued command at its FULL wrapped height
|
|
2596
|
-
// (same content width the promoted transcript user row wraps at), pinned
|
|
2597
|
-
// above the prompt box with no extra top-margin row. Reserving the true
|
|
2598
|
-
// height keeps promotion from re-expanding the text mid-flight ("row jump").
|
|
2599
|
-
// If the whole queue would eat too much of the frame, fall back to the old
|
|
2600
|
-
// compact 1-row-per-entry truncation so the input box never leaves screen.
|
|
2601
|
-
const queuedFullRows = queuedVisible
|
|
2602
|
-
? state.queued.reduce(
|
|
2603
|
-
(sum, item) => sum + queuedBandRows(String(item.displayText || item.text || ''), Math.max(1, frameColumns - 4)),
|
|
2604
|
-
0,
|
|
2605
|
-
)
|
|
2606
|
-
: 0;
|
|
2607
|
-
const queuedRowBudget = Math.max(3, Math.floor(resizeState.rows / 3));
|
|
2608
|
-
const queuedCompact = queuedFullRows > queuedRowBudget;
|
|
2609
|
-
const queuedRows = queuedVisible ? (queuedCompact ? state.queued.length : queuedFullRows) : 0;
|
|
2610
|
-
const INPUT_BOX_ROWS = promptBoxRows + promptMetaRows + overlayHintRows;
|
|
2611
|
-
// Welcome banner visibility (computed here, after the prompt/panel row math
|
|
2612
|
-
// it depends on). The slash palette must NOT unmount the banner on the empty
|
|
2613
|
-
// transcript: dropping the 11-row banner the moment '/' opened the palette
|
|
2614
|
-
// (and remounting it on Esc) read as the whole top of the screen jumping on
|
|
2615
|
-
// command-palette entry. Keep the banner mounted through a slash session
|
|
2616
|
-
// whenever the terminal is tall enough to hold banner + palette + prompt +
|
|
2617
|
-
// statusline at once; short terminals still yield the banner so the palette
|
|
2618
|
-
// chrome never clips (the floating container clips from the top).
|
|
2619
|
-
const WELCOME_BANNER_ROWS = 11;
|
|
2620
|
-
const SLASH_PALETTE_ROWS = PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS;
|
|
2621
|
-
const slashKeepsWelcomeBanner = slashPaletteOpen
|
|
2622
|
-
&& resizeState.rows >= WELCOME_BANNER_ROWS + SLASH_PALETTE_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + 1;
|
|
2623
|
-
const showWelcomeBanner = (state.items.length === 0 && (!hasFloatingPanel || slashKeepsWelcomeBanner))
|
|
2624
|
-
|| projectSelectionActive || onboardingActive;
|
|
2625
|
-
const WELCOME_ROWS = showWelcomeBanner ? WELCOME_BANNER_ROWS : 0;
|
|
2626
|
-
const baseReserve = WELCOME_ROWS + SCROLL_HINT_ROWS + LIVE_STATUS_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows;
|
|
2627
|
-
const maxFloatingPanelRows = Math.max(0, resizeState.rows - baseReserve - 1);
|
|
2628
|
-
const desiredFloatingPanelRows = toolApproval
|
|
2629
|
-
? PANEL_CHROME_ROWS + 2 + OPTION_PANEL_EXTRA_ROWS
|
|
2630
|
-
: picker
|
|
2631
|
-
? (picker.fillAvailable ? maxFloatingPanelRows : PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS)
|
|
2632
|
-
: contextPanel
|
|
2633
|
-
? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS + 3
|
|
2634
|
-
: usagePanel
|
|
2635
|
-
? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS
|
|
2636
|
-
: slashPaletteOpen
|
|
2637
|
-
? PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS
|
|
2638
|
-
: hasTextEntryPrompt
|
|
2639
|
-
? TEXT_ENTRY_ROWS
|
|
2640
|
-
: 0;
|
|
2641
|
-
const floatingPanelRows = desiredFloatingPanelRows > 0
|
|
2642
|
-
? Math.min(desiredFloatingPanelRows, maxFloatingPanelRows)
|
|
2643
|
-
: 0;
|
|
2644
|
-
// Give the list every content row the panel exposes. The panel already grew
|
|
2645
|
-
// by OPTION_PANEL_EXTRA_ROWS; previously that growth was subtracted back out
|
|
2646
|
-
// here, so the rows leaked into an empty flexGrow gap instead of the list.
|
|
2647
|
-
// Reserving only PICKER_CHROME_ROWS lets the list occupy the full interior
|
|
2648
|
-
// (the footer's own reservation is handled inside Picker).
|
|
2649
|
-
const pickerVisibleRows = picker
|
|
2650
|
-
? Math.max(1, floatingPanelRows - PICKER_CHROME_ROWS)
|
|
2651
|
-
: PANEL_MAX_VISIBLE;
|
|
2652
|
-
const rawBottomReserve = baseReserve + floatingPanelRows;
|
|
2653
|
-
const bottomClusterRows = INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows + floatingPanelRows;
|
|
2654
|
-
const panelLayoutSignature = [
|
|
2655
|
-
toolApproval ? 'tool' : '',
|
|
2656
|
-
picker ? `picker:${picker.kind || ''}:${picker.fillAvailable ? 'fill' : 'fit'}` : '',
|
|
2657
|
-
contextPanel ? 'context' : '',
|
|
2658
|
-
usagePanel ? 'usage' : '',
|
|
2659
|
-
slashPaletteOpen ? 'slash' : '',
|
|
2660
|
-
hasTextEntryPrompt ? `text:${textEntryPrompt?.kind || ''}` : '',
|
|
2661
|
-
inputBoxHidden ? 'input-hidden' : 'input-visible',
|
|
2662
|
-
floatingPanelRows,
|
|
1086
|
+
const {
|
|
1087
|
+
inputBoxHidden,
|
|
1088
|
+
latestTranscriptItem,
|
|
2663
1089
|
promptBoxRows,
|
|
1090
|
+
STATUSLINE_ROWS,
|
|
2664
1091
|
promptMetaRows,
|
|
1092
|
+
overlayHintRequested,
|
|
2665
1093
|
queuedRows,
|
|
2666
1094
|
WELCOME_ROWS,
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
// while no panel opened/closed/changed kind and floatingPanelRows itself is
|
|
2677
|
-
// unchanged — see PANEL_KIND_TOKEN_COUNT) must not fall into the clearRows
|
|
2678
|
-
// + setTimeout recommit path below: that inserts a full extra blank row for
|
|
2679
|
-
// one commit, which IS the newline-add/remove jolt while typing. Route a
|
|
2680
|
-
// shrink here through the same one-commit ink-mask path as an instant panel
|
|
2681
|
-
// close so the stale row is masked in the very commit it disappears; growth
|
|
2682
|
-
// already needs no clearance (panelShrinkRows is 0 in that case).
|
|
2683
|
-
const promptRowsOnlyChange = panelShrinkRows > 0
|
|
2684
|
-
&& panelKindSignature(panelTransition.signature) === panelKindSignature(panelLayoutSignature);
|
|
2685
|
-
// Turn-end spinner meta collapse: the 2-row live-spinner band disappears in
|
|
2686
|
-
// the SAME commit the engine appends the turndone/statusdone tail (see
|
|
2687
|
-
// engine.mjs runTurn — turndone + spinner:null land in one set()). That new
|
|
2688
|
-
// done row already replaces the vacated height, so masking those rows blank
|
|
2689
|
-
// for one commit only to clear them on the next commit IS the visible
|
|
2690
|
-
// transcript bounce. Exempt exactly the meta-collapse rows from the ink mask
|
|
2691
|
-
// when the done row is the transcript tail. Every other prompt-row-only
|
|
2692
|
-
// shrink (typing newline removal, queued-row churn) AND the reclaimed/no-op
|
|
2693
|
-
// path (engine.mjs skips turndone, so the completed-tail check stays false and no
|
|
2694
|
-
// row replaces the height) keep the mask so they still reclaim smoothly.
|
|
2695
|
-
const prevMetaRows = Number(String(panelTransition.signature).split('|')[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
|
|
2696
|
-
const nextMetaRows = Number(String(panelLayoutSignature).split('|')[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
|
|
2697
|
-
// Require the done row to have been appended in THIS commit (tail id changed
|
|
2698
|
-
// since the last commit). A command spinner can leave a STALE done row at the
|
|
2699
|
-
// tail and then clear without appending statusdone (e.g. /recall — see
|
|
2700
|
-
// engine.mjs), collapsing the meta band with NO same-commit backfill; masking
|
|
2701
|
-
// must stay on for that path or the vacated rows overpaint the stale row.
|
|
2702
|
-
const doneTailAppendedThisCommit = isCompletedTranscriptTailAppendedThisCommit(
|
|
2703
|
-
latestTranscriptItem,
|
|
2704
|
-
panelTransition.tailId,
|
|
2705
|
-
);
|
|
2706
|
-
const spinnerMetaCollapseRows = doneTailAppendedThisCommit
|
|
2707
|
-
? Math.max(0, prevMetaRows - nextMetaRows)
|
|
2708
|
-
: 0;
|
|
2709
|
-
// Queued-band promotion: drain() removes the queued band and appends the
|
|
2710
|
-
// promoted user transcript row in the SAME commit (session-flow.mjs drain
|
|
2711
|
-
// → pushUserOrSyntheticItem → runTurn spinner, one microtask flush). The
|
|
2712
|
-
// new user row (full wrapped height + margin) already backfills the
|
|
2713
|
-
// vacated band rows, so masking them blank for one commit only to drop
|
|
2714
|
-
// the mask on the next commit made the whole transcript bounce down.
|
|
2715
|
-
// Exempt exactly the vacated queued rows when a user row landed at the
|
|
2716
|
-
// tail in this commit; queue edits/removals without a tail append (tail
|
|
2717
|
-
// id unchanged, or non-user tail) keep the mask.
|
|
2718
|
-
const prevQueuedSigRows = Number(String(panelTransition.signature).split('|')[PANEL_LAYOUT_SIG.QUEUED]) || 0;
|
|
2719
|
-
const nextQueuedSigRows = Number(String(panelLayoutSignature).split('|')[PANEL_LAYOUT_SIG.QUEUED]) || 0;
|
|
2720
|
-
const userTailAppendedThisCommit = latestTranscriptItem?.kind === 'user'
|
|
2721
|
-
&& (latestTranscriptItem?.id ?? null) !== panelTransition.tailId;
|
|
2722
|
-
const queuedPromoteCollapseRows = userTailAppendedThisCommit
|
|
2723
|
-
? Math.max(0, prevQueuedSigRows - nextQueuedSigRows)
|
|
2724
|
-
: 0;
|
|
2725
|
-
const instantPanelClose = panelShrinkRows > 0
|
|
2726
|
-
&& (promptRowsOnlyChange
|
|
2727
|
-
|| isInstantPanelCloseTransition(panelTransition.signature, panelLayoutSignature, initialProjectEntryClose));
|
|
2728
|
-
// Slash palette opening on the empty welcome screen: bottomReserve already
|
|
2729
|
-
// grows to its final size in this same commit (floatingPanelRows reflects
|
|
2730
|
-
// slashPaletteOpen immediately, no clearRows needed), but the renderer can
|
|
2731
|
-
// still overpaint the just-vacated transcript row for one commit. Borrow
|
|
2732
|
-
// the transitional guard-row mechanism (normally used for tall panel
|
|
2733
|
-
// closes below) for exactly one commit on the open transition itself —
|
|
2734
|
-
// this only carves an extra blank row out of transcriptContentHeight, it
|
|
2735
|
-
// does not touch bottomReserve/floatingPanelRows/palette height.
|
|
2736
|
-
const slashOpenOnEmptyTranscript = initialProjectEntryClose
|
|
2737
|
-
&& !panelSignatureFlags(panelTransition.signature).slash
|
|
2738
|
-
&& panelSignatureFlags(panelLayoutSignature).slash;
|
|
2739
|
-
if (instantPanelClose) {
|
|
2740
|
-
// Slash palette and initial project-entry closes land on the final bottom
|
|
2741
|
-
// reserve in one commit. Paint reclaimed rows as a blank mask band below
|
|
2742
|
-
// the transcript clip instead of inflating bottomReserve + reclaiming on
|
|
2743
|
-
// the next tick. Subtract any turn-end spinner-meta rows that the same-
|
|
2744
|
-
// commit done tail already backfills (spinnerMetaCollapseRows) so that
|
|
2745
|
-
// transition masks nothing and does not bounce; same for queued-band
|
|
2746
|
-
// rows backfilled by a just-promoted user row (queuedPromoteCollapseRows).
|
|
2747
|
-
panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows - queuedPromoteCollapseRows);
|
|
2748
|
-
panelTransition.clearRows = 0;
|
|
2749
|
-
panelTransition.guardRows = 0;
|
|
2750
|
-
panelTransition.epoch = panelTransitionEpoch;
|
|
2751
|
-
} else if (slashOpenOnEmptyTranscript) {
|
|
2752
|
-
panelTransitionClearRows = 0;
|
|
2753
|
-
panelTransitionGuardRows = 1;
|
|
2754
|
-
panelTransition.clearRows = 0;
|
|
2755
|
-
panelTransition.guardRows = 1;
|
|
2756
|
-
panelTransition.epoch = panelTransitionEpoch;
|
|
2757
|
-
} else {
|
|
2758
|
-
// Tall panel closes must land on the final bottom reserve in the same
|
|
2759
|
-
// commit. Inflating bottomReserve with temporary clearance makes the
|
|
2760
|
-
// transcript/prompt area move once, then snap back on the timer commit.
|
|
2761
|
-
// Instead, keep the reclaimed rows inside the fixed viewport as a blank
|
|
2762
|
-
// one-frame mask, matching the instant-close path above.
|
|
2763
|
-
panelCloseInkMaskRowsRef.current = panelShrinkRows;
|
|
2764
|
-
panelTransition.clearRows = 0;
|
|
2765
|
-
panelTransition.guardRows = 0;
|
|
2766
|
-
panelTransition.epoch = panelTransitionEpoch;
|
|
2767
|
-
}
|
|
2768
|
-
} else if (panelTransition.epoch === panelTransitionEpoch) {
|
|
2769
|
-
panelTransitionClearRows = panelTransition.clearRows || 0;
|
|
2770
|
-
panelTransitionGuardRows = panelTransition.guardRows || 0;
|
|
2771
|
-
}
|
|
2772
|
-
if (desiredFloatingPanelRows > 0) {
|
|
2773
|
-
panelCloseInkMaskRowsRef.current = 0;
|
|
2774
|
-
}
|
|
2775
|
-
void panelInkMaskEpoch;
|
|
2776
|
-
const panelCloseInkMaskRows = desiredFloatingPanelRows > 0 ? 0 : panelCloseInkMaskRowsRef.current;
|
|
2777
|
-
const bottomReserve = rawBottomReserve + panelTransitionClearRows;
|
|
2778
|
-
const viewportHeight = Math.max(1, resizeState.rows - bottomReserve);
|
|
2779
|
-
// Keep one physical row between the transcript clip and the bottom cluster
|
|
2780
|
-
// even when pinned to the live tail. Windows Terminal/conhost can still
|
|
2781
|
-
// surface one clipped/off-by-one transcript row below the statusline during
|
|
2782
|
-
// rapid tool-card updates; a permanent guard row makes that row blank instead
|
|
2783
|
-
// of a tool header/detail.
|
|
2784
|
-
const guardCapacityRows = Math.max(0, viewportHeight - 1);
|
|
2785
|
-
const baseGuardRows = guardCapacityRows > 0 ? 1 : 0;
|
|
2786
|
-
// ── Scroll-time overprint guard ───────────────────────────────────────────
|
|
2787
|
-
// Wheel/manual scroll pushes the transcript column DOWN via a negative
|
|
2788
|
-
// marginBottom (see the viewport render). Under conhost/Windows Terminal the
|
|
2789
|
-
// incremental redraw can leave the row that slid past the clip edge painted
|
|
2790
|
-
// OVER the bottom cluster (input box / statusline) for a frame — the reported
|
|
2791
|
-
// "scrolled text shows on the statusline row" bug. One guard row is enough
|
|
2792
|
-
// while pinned to the live tail, but during an active scroll the slid row can
|
|
2793
|
-
// still bleed one line further, so widen the guard to TWO rows whenever the
|
|
2794
|
-
// viewport is genuinely scrolled up. The extra blank row absorbs the stray
|
|
2795
|
-
// paint instead of the statusline. Requires a viewport tall enough to spare
|
|
2796
|
-
// the row, and never shrinks below the base guard.
|
|
2797
|
-
// Gate on the same follow-aware basis the transcript window uses for
|
|
2798
|
-
// renderScrollOffset (see use-transcript-window.mjs targetNearBottom): the
|
|
2799
|
-
// live `scrollOffset` state can still read >0 for one frame after a wheel
|
|
2800
|
-
// turn re-arms bottom-follow, while this same frame already renders with
|
|
2801
|
-
// renderScrollOffset=0. Without this, the stale offset shrinks
|
|
2802
|
-
// transcriptContentHeight by one row that never actually gets rendered,
|
|
2803
|
-
// producing a one-row bounce that snaps back once state catches up.
|
|
2804
|
-
// [2026-07-06] Scroll-time extra guard DISABLED: the widened (2-row) guard
|
|
2805
|
-
// rendered as a visibly empty band between the transcript and the prompt box
|
|
2806
|
-
// whenever the viewport was scrolled up (user-reported "bottom rows look
|
|
2807
|
-
// blank while scrolling"). The base 1-row guard below stays; if the
|
|
2808
|
-
// scrolled-row-over-statusline overpaint resurfaces, fix it in the renderer
|
|
2809
|
-
// diff (clip/erase) instead of carving more blank viewport rows.
|
|
2810
|
-
const scrollGuardRows = 0;
|
|
2811
|
-
const transcriptGuardRows = Math.min(guardCapacityRows, baseGuardRows + panelTransitionGuardRows + scrollGuardRows);
|
|
2812
|
-
// Welcome prompt hint: a one-row band rendered INSIDE the transcript
|
|
2813
|
-
// viewport (as a sibling below the content clip), so it must be part of the
|
|
2814
|
-
// viewport row accounting computed right below. Left unaccounted, the slash
|
|
2815
|
-
// palette close commit on the empty welcome screen painted one extra
|
|
2816
|
-
// physical row (hint reappears in the same commit as the close mask), so
|
|
2817
|
-
// the prompt box + statusline dipped one row and snapped back on the
|
|
2818
|
-
// mask-clear commit.
|
|
2819
|
-
const welcomePromptHintText = conditionalWelcomePromptHint || welcomePromptHintRef.current || '';
|
|
2820
|
-
const welcomePromptHintVisible = Boolean(
|
|
2821
|
-
welcomePromptHintText
|
|
2822
|
-
&& !welcomePromptHintDismissed
|
|
2823
|
-
&& state.items.length === 0
|
|
2824
|
-
&& !hasFloatingPanel
|
|
2825
|
-
&& !inputBoxHidden
|
|
2826
|
-
&& !queuedVisible
|
|
2827
|
-
&& !liveSpinner
|
|
2828
|
-
&& !inputHint
|
|
2829
|
-
);
|
|
2830
|
-
// Tiny terminals: guard rows can already consume all but one viewport row
|
|
2831
|
-
// (guardCapacityRows = viewportHeight - 1). transcriptContentHeight clamps
|
|
2832
|
-
// to >= 1, so an unconditional hint row would paint viewportHeight + 1 rows
|
|
2833
|
-
// and push the prompt/statusline down. The hint yields unless at least one
|
|
2834
|
-
// content row remains beside it.
|
|
2835
|
-
const welcomePromptHintRows = welcomePromptHintVisible
|
|
2836
|
-
&& (viewportHeight - transcriptGuardRows) >= 2 ? 1 : 0;
|
|
2837
|
-
welcomePromptHintVisibleRef.current = welcomePromptHintRows > 0;
|
|
2838
|
-
// Transient hint/error on the EMPTY transcript: the guard row sits directly
|
|
2839
|
-
// above the prompt box, so painting the hint there hugs the textbox one row
|
|
2840
|
-
// below where the live-spinner line renders. Carve one in-viewport row ABOVE
|
|
2841
|
-
// the guard row instead so the hint's baseline matches the spinner row (two
|
|
2842
|
-
// rows above the box, guard row stays blank as the spacer). This is an
|
|
2843
|
-
// in-viewport carve like welcomePromptHintRows — bottomReserve is untouched,
|
|
2844
|
-
// so the prompt box and statusline never move. Non-empty transcripts keep the
|
|
2845
|
-
// existing attach-to-last-item / guard-row fallback placements.
|
|
2846
|
-
const overlayHintBandRows = overlayHintRequested
|
|
2847
|
-
&& state.items.length === 0
|
|
2848
|
-
&& (viewportHeight - transcriptGuardRows - welcomePromptHintRows) >= 2 ? 1 : 0;
|
|
2849
|
-
// Instant panel close (slash palette): the reclaimed rows stay blank for
|
|
2850
|
-
// exactly one commit via panelCloseMaskRows. The mask MUST be part of this
|
|
2851
|
-
// frame's row accounting — subtract it from the transcript content height
|
|
2852
|
-
// and render it as a sibling band below the content clip (where the closed
|
|
2853
|
-
// panel's ink was). Rendering it inside the scrolled transcript column made
|
|
2854
|
-
// the painted column taller than the accounted viewport for one frame, so
|
|
2855
|
-
// the prompt/statusline dropped a row and snapped back on the mask-clear
|
|
2856
|
-
// commit (the "textbox dips when the slash palette closes" bug).
|
|
2857
|
-
const panelCloseMaskRows = Math.min(
|
|
2858
|
-
panelCloseInkMaskRows,
|
|
2859
|
-
Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - overlayHintBandRows - 1),
|
|
2860
|
-
);
|
|
2861
|
-
const transcriptContentHeight = Math.max(
|
|
2862
|
-
1,
|
|
2863
|
-
viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows - overlayHintBandRows,
|
|
2864
|
-
);
|
|
2865
|
-
// Keep the keyboard-selection edge step anchored to the base guard. This is
|
|
2866
|
-
// not a follow threshold: any positive wheel target is a reading position;
|
|
2867
|
-
// only the true bottom may auto-follow a live tail.
|
|
2868
|
-
const transcriptBottomSlackRows = Math.max(0, baseGuardRows);
|
|
2869
|
-
transcriptBottomSlackRowsRef.current = transcriptBottomSlackRows;
|
|
2870
|
-
transcriptViewportRef.current = {
|
|
2871
|
-
top: WELCOME_ROWS,
|
|
2872
|
-
bottom: Math.max(WELCOME_ROWS, WELCOME_ROWS + transcriptContentHeight - 1),
|
|
2873
|
-
};
|
|
2874
|
-
// [mixdog] Keep the live terminal row count current for the mouse handler's
|
|
2875
|
-
// region routing + status-band selection clip (see onData).
|
|
2876
|
-
frameRowsRef.current = Math.max(1, Number(resizeState.rows) || 24);
|
|
2877
|
-
// When the prompt box is hidden (floating panel / option panel owns the
|
|
2878
|
-
// bottom area), drop its stale measured rect so the mouse handler does not
|
|
2879
|
-
// route presses to a prompt box that is not on screen.
|
|
2880
|
-
if (inputBoxHidden) promptBoxRectRef.current = null;
|
|
2881
|
-
// Toast/error text has two mutually exclusive placements:
|
|
2882
|
-
// - while a live status row exists (thinking/compacting/responding), attach it
|
|
2883
|
-
// to that row so the bottom cluster reserves exactly one status band;
|
|
2884
|
-
// - otherwise render it into the normal one-row gap above the prompt, replacing
|
|
2885
|
-
// the blank spacer instead of reserving an extra row. This keeps late errors
|
|
2886
|
-
// from pushing the prompt/statusline upward, and when thinking starts the
|
|
2887
|
-
// hint moves into the live row on the same render instead of double-painting.
|
|
2888
|
-
// Transient hint placement while no spinner owns the band is resolved after
|
|
2889
|
-
// transcript windowing (see overlayHintOnLastItem / overlayHintFallbackRow).
|
|
2890
|
-
const spinnerHintWidth = inputHint
|
|
2891
|
-
? Math.max(1, Math.min(Math.max(1, frameColumns - 4), Math.max(12, Math.floor(frameColumns * 0.42))))
|
|
2892
|
-
: 0;
|
|
2893
|
-
// When no live spinner owns a status band, the transient hint/error is drawn
|
|
2894
|
-
// into the existing transcript guard row directly above the prompt. Mirror the
|
|
2895
|
-
// spinner-row placement: a fixed-width right slot, not a full-width left box.
|
|
2896
|
-
const guardHintWidth = inputHint
|
|
2897
|
-
? Math.max(1, Math.min(Math.max(1, frameColumns - 4), Math.max(12, Math.floor(frameColumns * 0.42))))
|
|
2898
|
-
: 0;
|
|
2899
|
-
const transientStatusWidth = liveSpinner ? spinnerHintWidth : guardHintWidth;
|
|
2900
|
-
const promptSpinnerColumns = liveSpinner && inputHint
|
|
2901
|
-
? Math.max(1, frameColumns - spinnerHintWidth - 1)
|
|
2902
|
-
: frameColumns;
|
|
1095
|
+
floatingPanelRows,
|
|
1096
|
+
bottomClusterRows,
|
|
1097
|
+
panelLayoutSignature,
|
|
1098
|
+
panelTransitionClearRows,
|
|
1099
|
+
panelTransitionGuardRows,
|
|
1100
|
+
transcriptGuardRows,
|
|
1101
|
+
transcriptContentHeight,
|
|
1102
|
+
transcriptBottomSlackRows,
|
|
1103
|
+
} = layout;
|
|
2903
1104
|
useEffect(() => {
|
|
2904
1105
|
const transition = panelTransitionRef.current;
|
|
2905
1106
|
const pendingInkMask = panelCloseInkMaskRowsRef.current;
|
|
@@ -3015,484 +1216,69 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
3015
1216
|
// The hardware/IME caret is parked by PromptInput from its OWN measured box
|
|
3016
1217
|
// position (ink useCursor + useBoxMetrics) — correct now that the transcript
|
|
3017
1218
|
// is a live column, so the live-frame line count ink relies on is accurate.
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
<Box height={1} flexShrink={0} />
|
|
3084
|
-
<Text color={theme.inactive}>{centerLine(`mixdog coding agent · v${localPackageVersion()} · ${state.cwd}`, frameColumns, 4)}</Text>
|
|
3085
|
-
</Box>
|
|
3086
|
-
) : null}
|
|
3087
|
-
|
|
3088
|
-
{/* Transcript viewport — a BOUNDED, fixed-height clipping box. The explicit
|
|
3089
|
-
numeric height + overflow:hidden is what lets ink actually slice the
|
|
3090
|
-
off-screen rows (output.clip in render-node-to-output.js), so older
|
|
3091
|
-
rows can never overprint newer ones. justifyContent flex-end keeps the
|
|
3092
|
-
newest content pinned to the bottom edge; older content overflows the
|
|
3093
|
-
TOP and is clipped. flexShrink lets it yield rows to the live status /
|
|
3094
|
-
a multi-line input rather than overflow the screen. */}
|
|
3095
|
-
<Box
|
|
3096
|
-
flexDirection="column"
|
|
3097
|
-
width="100%"
|
|
3098
|
-
height={viewportHeight}
|
|
3099
|
-
flexGrow={0}
|
|
3100
|
-
flexShrink={1}
|
|
3101
|
-
overflow="hidden"
|
|
3102
|
-
justifyContent="flex-end"
|
|
3103
|
-
>
|
|
3104
|
-
<Box
|
|
3105
|
-
flexDirection="column"
|
|
3106
|
-
width="100%"
|
|
3107
|
-
height={transcriptContentHeight}
|
|
3108
|
-
flexShrink={0}
|
|
3109
|
-
overflow="hidden"
|
|
3110
|
-
justifyContent="flex-end"
|
|
3111
|
-
>
|
|
3112
|
-
{/* Wheel scroll: with the viewport bottom-anchored (flex-end), a NEGATIVE
|
|
3113
|
-
marginBottom pushes the transcript column DOWN past the bottom edge,
|
|
3114
|
-
bringing older content above the window into view (overflow hidden
|
|
3115
|
-
clips the newest rows that slide below). 0 = newest content pinned to
|
|
3116
|
-
the bottom. (marginTop has no effect under flex-end — the bottom edge
|
|
3117
|
-
stays fixed — so the scroll axis here is marginBottom, not marginTop.)
|
|
3118
|
-
scrollOffset is clamped ≥ 0 by the wheel handler; a new turn snaps it
|
|
3119
|
-
back to 0. */}
|
|
3120
|
-
<Box flexDirection="column" width="100%" flexShrink={0} marginBottom={-transcriptWindow.effectiveScrollOffset}>
|
|
3121
|
-
{/*
|
|
3122
|
-
* Transcript windowing: render only the rows around the viewport rather
|
|
3123
|
-
* than the full state.items list. A cheap bottom spacer preserves the
|
|
3124
|
-
* same scroll coordinate when the visible window is in older history;
|
|
3125
|
-
* items above the window are off-screen and omitted entirely.
|
|
3126
|
-
* MAX cap: TRANSCRIPT_WINDOW_MAX_ITEMS items (env MIXDOG_TUI_TRANSCRIPT_WINDOW_ITEMS).
|
|
3127
|
-
* OVERSCAN: TRANSCRIPT_WINDOW_OVERSCAN_ROWS extra rows above the viewport so
|
|
3128
|
-
* fast wheel scrolls don't show a blank gap before re-render.
|
|
3129
|
-
*/}
|
|
3130
|
-
{renderedTranscriptItems.map((item, i, arr) => {
|
|
3131
|
-
const measureRef = transcriptMeasureRef(item);
|
|
3132
|
-
const attachOverlayHint = overlayHintOnLastItem && i === overlayHintAttachItemIndex;
|
|
3133
|
-
const itemNode = (
|
|
3134
|
-
<Item
|
|
3135
|
-
item={item}
|
|
3136
|
-
prevKind={i > 0
|
|
3137
|
-
? arr[i - 1].kind
|
|
3138
|
-
: (state.transcriptViewItems || state.items)[transcriptWindow.startIndex - 1]?.kind ?? null}
|
|
3139
|
-
columns={frameColumns}
|
|
3140
|
-
toolOutputExpanded={toolOutputExpanded}
|
|
3141
|
-
rightMessage={attachOverlayHint ? inputHint : ''}
|
|
3142
|
-
rightTone={attachOverlayHint ? inputHintTone : 'info'}
|
|
3143
|
-
rightMessageWidth={attachOverlayHint ? (guardHintWidth || transientStatusWidth || 24) : 24}
|
|
3144
|
-
themeEpoch={state.themeEpoch || 0}
|
|
3145
|
-
streamingWindowRows={transcriptTailPinned && item.id === state.streamingTail?.id
|
|
3146
|
-
? transcriptContentHeight + TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS
|
|
3147
|
-
: 0}
|
|
3148
|
-
/>
|
|
3149
|
-
);
|
|
3150
|
-
// When measured-rows is on, wrap each row in a zero-cost flex column
|
|
3151
|
-
// whose ref exposes the row's REAL Yoga height to the harvest effect.
|
|
3152
|
-
// The wrapper adds no rows of its own (it shrink-wraps the child) and
|
|
3153
|
-
// is omitted entirely when the feature is disabled so the default
|
|
3154
|
-
// render tree is byte-for-byte unchanged on the off path.
|
|
3155
|
-
return measureRef ? (
|
|
3156
|
-
<Box key={item.id} ref={measureRef} flexDirection="column" flexShrink={0}>
|
|
3157
|
-
{itemNode}
|
|
3158
|
-
</Box>
|
|
3159
|
-
) : (
|
|
3160
|
-
<React.Fragment key={item.id}>{itemNode}</React.Fragment>
|
|
3161
|
-
);
|
|
3162
|
-
})}
|
|
3163
|
-
{transcriptWindow.bottomSpacerRows > 0 ? (
|
|
3164
|
-
<Box height={transcriptWindow.bottomSpacerRows} flexShrink={0} />
|
|
3165
|
-
) : null}
|
|
3166
|
-
</Box>
|
|
3167
|
-
</Box>
|
|
3168
|
-
{welcomePromptHintRows > 0 ? (
|
|
3169
|
-
<Box height={1} flexShrink={0} width="100%" overflow="hidden">
|
|
3170
|
-
<Text color={theme.inactive} wrap="truncate">{centerLine(welcomePromptHintText, frameColumns, 2)}</Text>
|
|
3171
|
-
</Box>
|
|
3172
|
-
) : null}
|
|
3173
|
-
{panelCloseMaskRows > 0 ? (
|
|
3174
|
-
<Box
|
|
3175
|
-
height={panelCloseMaskRows}
|
|
3176
|
-
flexShrink={0}
|
|
3177
|
-
width="100%"
|
|
3178
|
-
overflow="hidden"
|
|
3179
|
-
backgroundColor={surfaceBackground()}
|
|
3180
|
-
/>
|
|
3181
|
-
) : null}
|
|
3182
|
-
{overlayHintBandRows > 0 ? (
|
|
3183
|
-
<Box height={1} flexShrink={0} backgroundColor={surfaceBackground()} flexDirection="row" width="100%" overflow="hidden">
|
|
3184
|
-
<Box flexGrow={1} flexShrink={1} overflow="hidden" />
|
|
3185
|
-
<Box flexShrink={0} width={guardHintWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
|
|
3186
|
-
<Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
|
|
3187
|
-
</Box>
|
|
3188
|
-
</Box>
|
|
3189
|
-
) : null}
|
|
3190
|
-
{transcriptGuardRows > 0 ? (
|
|
3191
|
-
<Box height={transcriptGuardRows} flexShrink={0} backgroundColor={surfaceBackground()} flexDirection="row" width="100%" overflow="hidden">
|
|
3192
|
-
<Box flexGrow={1} flexShrink={1} overflow="hidden" />
|
|
3193
|
-
{overlayHintFallbackRow && overlayHintBandRows === 0 ? (
|
|
3194
|
-
<Box flexShrink={0} width={guardHintWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
|
|
3195
|
-
<Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
|
|
3196
|
-
</Box>
|
|
3197
|
-
) : null}
|
|
3198
|
-
</Box>
|
|
3199
|
-
) : null}
|
|
3200
|
-
</Box>
|
|
3201
|
-
|
|
3202
|
-
{/* Live reasoning and transient status live just above the prompt: reasoning
|
|
3203
|
-
on the left, short-lived copy/error/info messages on the right. */}
|
|
3204
|
-
|
|
3205
|
-
{/* Bottom bar — pinned to the physical bottom, never moves. Floating
|
|
3206
|
-
panels use their actual rendered height and shrink before the prompt
|
|
3207
|
-
can move; overflow is clipped from the top while the panel remains
|
|
3208
|
-
bottom-aligned against the prompt. */}
|
|
3209
|
-
<Box flexDirection="column" flexShrink={0} width="100%" backgroundColor={surfaceBackground()}>
|
|
3210
|
-
{panelTransitionClearRows > 0 ? (
|
|
3211
|
-
<Box height={panelTransitionClearRows} flexShrink={0} width="100%" overflow="hidden" backgroundColor={surfaceBackground()} />
|
|
3212
|
-
) : null}
|
|
3213
|
-
{floatingPanelRows > 0 ? (
|
|
3214
|
-
<Box flexDirection="column" flexShrink={0} height={floatingPanelRows} overflow="hidden" justifyContent="flex-end" backgroundColor={surfaceBackground()}>
|
|
3215
|
-
{toolApproval ? (
|
|
3216
|
-
<Picker
|
|
3217
|
-
items={[
|
|
3218
|
-
{ value: 'deny', label: 'Deny', marker: '×', markerColor: theme.error, description: 'block this tool call' },
|
|
3219
|
-
{ value: 'approve', label: 'Approve once', marker: '✓', markerColor: theme.success, description: 'run this tool call' },
|
|
3220
|
-
]}
|
|
3221
|
-
onSelect={(value) => {
|
|
3222
|
-
store.resolveToolApproval?.(toolApproval.id, {
|
|
3223
|
-
approved: value === 'approve',
|
|
3224
|
-
reason: value === 'approve' ? 'approved by user' : 'denied by user',
|
|
3225
|
-
});
|
|
3226
|
-
}}
|
|
3227
|
-
onCancel={() => {
|
|
3228
|
-
store.resolveToolApproval?.(toolApproval.id, { approved: false, reason: 'denied by user' });
|
|
3229
|
-
}}
|
|
3230
|
-
onKey={(input) => {
|
|
3231
|
-
const value = String(input || '').trim().toLowerCase();
|
|
3232
|
-
if (value === 'a' || value === 'y') {
|
|
3233
|
-
store.resolveToolApproval?.(toolApproval.id, { approved: true, reason: 'approved by user' });
|
|
3234
|
-
} else if (value === 'd' || value === 'n') {
|
|
3235
|
-
store.resolveToolApproval?.(toolApproval.id, { approved: false, reason: 'denied by user' });
|
|
3236
|
-
}
|
|
3237
|
-
}}
|
|
3238
|
-
title="Tool approval"
|
|
3239
|
-
description={toolApprovalDescription(toolApproval)}
|
|
3240
|
-
help="↑/↓ Select · Enter Choose · a/y Approve · d/n/Esc Deny"
|
|
3241
|
-
columns={frameColumns}
|
|
3242
|
-
labelWidth={18}
|
|
3243
|
-
initialIndex={0}
|
|
3244
|
-
indexMode="never"
|
|
3245
|
-
visibleCount={2}
|
|
3246
|
-
fillHeight={expandedOptionPanel}
|
|
3247
|
-
/>
|
|
3248
|
-
) : picker ? (
|
|
3249
|
-
<Picker
|
|
3250
|
-
key={picker.pickerKey}
|
|
3251
|
-
items={picker.items}
|
|
3252
|
-
onSelect={(value, item) => {
|
|
3253
|
-
pickerOpenedFromEnterRef.current = true;
|
|
3254
|
-
if (pickerOpenedFromEnterTimerRef.current) {
|
|
3255
|
-
clearTimeout(pickerOpenedFromEnterTimerRef.current);
|
|
3256
|
-
pickerOpenedFromEnterTimerRef.current = null;
|
|
3257
|
-
}
|
|
3258
|
-
try {
|
|
3259
|
-
if (picker.onSelect) picker.onSelect(value, item);
|
|
3260
|
-
} finally {
|
|
3261
|
-
pickerOpenedFromEnterTimerRef.current = setTimeout(() => {
|
|
3262
|
-
pickerOpenedFromEnterRef.current = false;
|
|
3263
|
-
pickerOpenedFromEnterTimerRef.current = null;
|
|
3264
|
-
}, 3000);
|
|
3265
|
-
}
|
|
3266
|
-
}}
|
|
3267
|
-
onCancel={() => {
|
|
3268
|
-
if (picker.onCancel) picker.onCancel();
|
|
3269
|
-
else {
|
|
3270
|
-
setPicker(null);
|
|
3271
|
-
clearPromptHint();
|
|
3272
|
-
}
|
|
3273
|
-
}}
|
|
3274
|
-
onLeft={picker.onLeft}
|
|
3275
|
-
onRight={picker.onRight}
|
|
3276
|
-
onTab={picker.onTab}
|
|
3277
|
-
onKey={picker.onKey}
|
|
3278
|
-
onHighlight={picker.onHighlight}
|
|
3279
|
-
title={picker.title}
|
|
3280
|
-
description={picker.description}
|
|
3281
|
-
footer={picker.footer}
|
|
3282
|
-
footerGapRows={picker.footerGapRows}
|
|
3283
|
-
help={picker.help}
|
|
3284
|
-
columns={frameColumns}
|
|
3285
|
-
labelWidth={picker.labelWidth}
|
|
3286
|
-
metaWidth={picker.metaWidth}
|
|
3287
|
-
initialIndex={picker.initialIndex}
|
|
3288
|
-
indexMode={picker.indexMode}
|
|
3289
|
-
visibleCount={pickerVisibleRows}
|
|
3290
|
-
fillHeight={expandedOptionPanel}
|
|
3291
|
-
themeEpoch={state.themeEpoch || 0}
|
|
3292
|
-
confirmBar={picker.confirmBar}
|
|
3293
|
-
/>
|
|
3294
|
-
) : contextPanel ? (
|
|
3295
|
-
<ContextPanel
|
|
3296
|
-
rows={contextPanel.rows}
|
|
3297
|
-
title={contextPanel.title}
|
|
3298
|
-
detail={contextPanel.detail}
|
|
3299
|
-
columns={frameColumns}
|
|
3300
|
-
fillHeight={expandedOptionPanel}
|
|
3301
|
-
/>
|
|
3302
|
-
) : usagePanel ? (
|
|
3303
|
-
<UsagePanel
|
|
3304
|
-
dashboard={usagePanel}
|
|
3305
|
-
columns={frameColumns}
|
|
3306
|
-
fillHeight={expandedOptionPanel}
|
|
3307
|
-
panelRows={floatingPanelRows}
|
|
3308
|
-
/>
|
|
3309
|
-
) : slashPaletteOpen ? (
|
|
3310
|
-
<SlashCommandPalette
|
|
3311
|
-
commands={slashCommands}
|
|
3312
|
-
selectedIndex={slashIndex}
|
|
3313
|
-
title="Commands"
|
|
3314
|
-
columns={frameColumns}
|
|
3315
|
-
query={activeSlashQuery}
|
|
3316
|
-
/>
|
|
3317
|
-
) : providerPrompt ? (
|
|
3318
|
-
<TextEntryPanel
|
|
3319
|
-
title={providerPrompt.kind === 'api-key'
|
|
3320
|
-
? `${providerPrompt.mode === 'replace' ? 'Replace' : 'Set'} API key · ${providerPrompt.label}`
|
|
3321
|
-
: providerPrompt.kind === 'oauth-code'
|
|
3322
|
-
? providerPrompt.label
|
|
3323
|
-
: providerPrompt.kind === 'openai-usage-session'
|
|
3324
|
-
? 'OpenAI Usage · Session Key'
|
|
3325
|
-
: `Base URL · ${providerPrompt.label}`}
|
|
3326
|
-
hint={providerPrompt.kind === 'api-key'
|
|
3327
|
-
? [
|
|
3328
|
-
providerPrompt.envName ? `Env: ${providerPrompt.envName}` : '',
|
|
3329
|
-
providerPrompt.source ? `Current: ${providerPrompt.source}` : '',
|
|
3330
|
-
'Stored in the OS keychain.',
|
|
3331
|
-
].filter(Boolean).join(' · ')
|
|
3332
|
-
: providerPrompt.kind === 'oauth-code'
|
|
3333
|
-
? (providerPrompt.hint || 'Paste the browser code.')
|
|
3334
|
-
: providerPrompt.kind === 'openai-usage-session'
|
|
3335
|
-
? 'Paste an OpenAI dashboard/session key for the undocumented credit lookup. It is stored in the OS keychain.'
|
|
3336
|
-
: `Default: ${providerPrompt.defaultURL}`}
|
|
3337
|
-
detail={providerPrompt.detail || ''}
|
|
3338
|
-
mask={providerPrompt.kind === 'api-key' || providerPrompt.kind === 'openai-usage-session'}
|
|
3339
|
-
columns={frameColumns}
|
|
3340
|
-
actionLabel={providerPrompt.kind === 'oauth-code' ? 'continue' : 'save'}
|
|
3341
|
-
promptLabel={providerPrompt.kind === 'api-key'
|
|
3342
|
-
? 'API key > '
|
|
3343
|
-
: providerPrompt.kind === 'oauth-code'
|
|
3344
|
-
? 'Paste code here if prompted > '
|
|
3345
|
-
: providerPrompt.kind === 'openai-usage-session'
|
|
3346
|
-
? 'Session key > '
|
|
3347
|
-
: 'Base URL > '}
|
|
3348
|
-
onSubmit={onSubmit}
|
|
3349
|
-
onCancel={cancelProviderPrompt}
|
|
3350
|
-
/>
|
|
3351
|
-
) : channelPrompt ? (
|
|
3352
|
-
<TextEntryPanel
|
|
3353
|
-
title={channelPrompt.label}
|
|
3354
|
-
hint={channelPrompt.hint || 'Save channel setting.'}
|
|
3355
|
-
mask={channelPrompt.kind === 'discord-token' || channelPrompt.kind === 'telegram-token' || channelPrompt.kind === 'webhook-token'}
|
|
3356
|
-
columns={frameColumns}
|
|
3357
|
-
promptLabel="Value > "
|
|
3358
|
-
onSubmit={onSubmit}
|
|
3359
|
-
onCancel={cancelChannelPrompt}
|
|
3360
|
-
/>
|
|
3361
|
-
) : hookPrompt ? (
|
|
3362
|
-
<TextEntryPanel
|
|
3363
|
-
title={hookPrompt.label}
|
|
3364
|
-
hint={hookPrompt.hint || 'Save hook setting.'}
|
|
3365
|
-
columns={frameColumns}
|
|
3366
|
-
promptLabel="Value > "
|
|
3367
|
-
onSubmit={onSubmit}
|
|
3368
|
-
onCancel={cancelHookPrompt}
|
|
3369
|
-
/>
|
|
3370
|
-
) : settingsPrompt ? (
|
|
3371
|
-
<TextEntryPanel
|
|
3372
|
-
title={settingsPrompt.label}
|
|
3373
|
-
hint={settingsPrompt.hint || 'Save setting.'}
|
|
3374
|
-
columns={frameColumns}
|
|
3375
|
-
initialValue={settingsPrompt.initialValue || ''}
|
|
3376
|
-
multiline={settingsPrompt.kind === 'core-add' || settingsPrompt.kind === 'core-edit'}
|
|
3377
|
-
maxContentRows={PANEL_MAX_VISIBLE}
|
|
3378
|
-
onContentRowsChange={setTextEntryLayoutRows}
|
|
3379
|
-
actionLabel={settingsPrompt.kind === 'skill-use'
|
|
3380
|
-
? 'run'
|
|
3381
|
-
: settingsPrompt.kind === 'autoclear-provider'
|
|
3382
|
-
? 'save'
|
|
3383
|
-
: settingsPrompt.kind === 'project-new'
|
|
3384
|
-
? 'open'
|
|
3385
|
-
: settingsPrompt.kind === 'project-create-confirm'
|
|
3386
|
-
? 'confirm'
|
|
3387
|
-
: settingsPrompt.kind === 'project-rename'
|
|
3388
|
-
? 'rename'
|
|
3389
|
-
: settingsPrompt.kind === 'core-add'
|
|
3390
|
-
? 'add'
|
|
3391
|
-
: settingsPrompt.kind === 'core-edit'
|
|
3392
|
-
? 'save'
|
|
3393
|
-
: settingsPrompt.kind === 'core-delete-confirm'
|
|
3394
|
-
? 'confirm'
|
|
3395
|
-
: 'save'}
|
|
3396
|
-
promptLabel={settingsPrompt.kind === 'skill-use'
|
|
3397
|
-
? 'Command > '
|
|
3398
|
-
: settingsPrompt.kind === 'autoclear-provider'
|
|
3399
|
-
? 'Duration > '
|
|
3400
|
-
: settingsPrompt.kind === 'project-new'
|
|
3401
|
-
? 'Path > '
|
|
3402
|
-
: settingsPrompt.kind === 'project-create-confirm'
|
|
3403
|
-
? 'Create? (y/n) > '
|
|
3404
|
-
: settingsPrompt.kind === 'project-rename'
|
|
3405
|
-
? 'Name > '
|
|
3406
|
-
: settingsPrompt.kind === 'core-add'
|
|
3407
|
-
? 'Sentence > '
|
|
3408
|
-
: settingsPrompt.kind === 'core-edit'
|
|
3409
|
-
? 'Sentence > '
|
|
3410
|
-
: settingsPrompt.kind === 'core-delete-confirm'
|
|
3411
|
-
? 'Delete? (y/n) > '
|
|
3412
|
-
: 'Value > '}
|
|
3413
|
-
onSubmit={onSubmit}
|
|
3414
|
-
onCancel={cancelSettingsPrompt}
|
|
3415
|
-
/>
|
|
3416
|
-
) : null}
|
|
3417
|
-
</Box>
|
|
3418
|
-
) : null}
|
|
3419
|
-
{!inputBoxHidden ? (
|
|
3420
|
-
<>
|
|
3421
|
-
{promptMetaVisible ? (
|
|
3422
|
-
<>
|
|
3423
|
-
<Box
|
|
3424
|
-
marginTop={0}
|
|
3425
|
-
marginBottom={0}
|
|
3426
|
-
height={1}
|
|
3427
|
-
width="100%"
|
|
3428
|
-
flexDirection="row"
|
|
3429
|
-
backgroundColor={surfaceBackground()}
|
|
3430
|
-
>
|
|
3431
|
-
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
3432
|
-
{liveSpinner ? (
|
|
3433
|
-
<Spinner
|
|
3434
|
-
verb={liveSpinner.verb}
|
|
3435
|
-
startedAt={liveSpinner.startedAt}
|
|
3436
|
-
outputTokens={liveSpinner?.outputTokens ?? liveSpinner?.tokens ?? 0}
|
|
3437
|
-
thinking={!!(state.thinking || liveSpinner?.thinking)}
|
|
3438
|
-
thinkingActiveSince={liveSpinner?.thinkingSegmentStartedAt ?? 0}
|
|
3439
|
-
mode={liveSpinner?.mode || 'responding'}
|
|
3440
|
-
columns={promptSpinnerColumns}
|
|
3441
|
-
marginTop={0}
|
|
3442
|
-
/>
|
|
3443
|
-
) : null}
|
|
3444
|
-
</Box>
|
|
3445
|
-
{inputHint ? (
|
|
3446
|
-
<Box flexShrink={0} width={transientStatusWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
|
|
3447
|
-
<Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
|
|
3448
|
-
</Box>
|
|
3449
|
-
) : null}
|
|
3450
|
-
</Box>
|
|
3451
|
-
<Box height={1} width="100%" backgroundColor={surfaceBackground()} />
|
|
3452
|
-
</>
|
|
3453
|
-
) : null}
|
|
3454
|
-
{queuedVisible ? (
|
|
3455
|
-
<QueuedCommands queued={state.queued} columns={frameColumns} compact={queuedCompact} />
|
|
3456
|
-
) : null}
|
|
3457
|
-
<Box
|
|
3458
|
-
marginTop={0}
|
|
3459
|
-
width="100%"
|
|
3460
|
-
height={promptBoxRows}
|
|
3461
|
-
flexShrink={0}
|
|
3462
|
-
borderStyle="round"
|
|
3463
|
-
borderColor={theme.promptBorder}
|
|
3464
|
-
backgroundColor={surfaceBackground()}
|
|
3465
|
-
paddingX={1}
|
|
3466
|
-
>
|
|
3467
|
-
{promptInputControl}
|
|
3468
|
-
</Box>
|
|
3469
|
-
</>
|
|
3470
|
-
) : null}
|
|
3471
|
-
<StatusLine
|
|
3472
|
-
sessionId={state.sessionId}
|
|
3473
|
-
clientHostPid={state.ownerClientHostPid || state.clientHostPid}
|
|
3474
|
-
provider={state.provider}
|
|
3475
|
-
model={state.model}
|
|
3476
|
-
effort={state.effort}
|
|
3477
|
-
fast={state.fast}
|
|
3478
|
-
cwd={state.cwd}
|
|
3479
|
-
stats={statuslineStats}
|
|
3480
|
-
contextWindow={state.contextWindow}
|
|
3481
|
-
displayContextWindow={state.displayContextWindow}
|
|
3482
|
-
compactBoundaryTokens={state.compactBoundaryTokens}
|
|
3483
|
-
autoCompactTokenLimit={state.autoCompactTokenLimit}
|
|
3484
|
-
rawContextWindow={state.rawContextWindow}
|
|
3485
|
-
resizeEpoch={resizeEpoch}
|
|
3486
|
-
agentRevision={agentRevision}
|
|
3487
|
-
agentWorkers={state.agentWorkers}
|
|
3488
|
-
agentJobs={state.agentJobs}
|
|
3489
|
-
activeTools={activeTools}
|
|
3490
|
-
initialLine={initialStatusLine}
|
|
3491
|
-
workflow={state.workflow}
|
|
3492
|
-
remoteEnabled={state.remoteEnabled === true}
|
|
3493
|
-
themeEpoch={state.themeEpoch || 0}
|
|
3494
|
-
/>
|
|
3495
|
-
</Box>
|
|
3496
|
-
</Box>
|
|
3497
|
-
);
|
|
1219
|
+
// Full view tree: app/app-view.jsx.
|
|
1220
|
+
return renderAppView({
|
|
1221
|
+
...layout,
|
|
1222
|
+
acceptSlashPalette,
|
|
1223
|
+
activeSlashQuery,
|
|
1224
|
+
activeTools,
|
|
1225
|
+
agentRevision,
|
|
1226
|
+
cancelChannelPrompt,
|
|
1227
|
+
cancelHookPrompt,
|
|
1228
|
+
cancelProviderPrompt,
|
|
1229
|
+
cancelSettingsPrompt,
|
|
1230
|
+
cancelSlashPalette,
|
|
1231
|
+
channelPrompt,
|
|
1232
|
+
clearPromptHint,
|
|
1233
|
+
completeSlashPalette,
|
|
1234
|
+
contextPanel,
|
|
1235
|
+
cycleWorkflowFromPrompt,
|
|
1236
|
+
exiting,
|
|
1237
|
+
frameColumns,
|
|
1238
|
+
gridSelectionActiveRef,
|
|
1239
|
+
handlePromptEscape,
|
|
1240
|
+
handlePromptHistoryNavigate,
|
|
1241
|
+
handlePromptInterrupt,
|
|
1242
|
+
handlePromptPaste,
|
|
1243
|
+
hookPrompt,
|
|
1244
|
+
initialStatusLine,
|
|
1245
|
+
onPromptDraftChange,
|
|
1246
|
+
onSubmit,
|
|
1247
|
+
overlayHintAttachItemIndex,
|
|
1248
|
+
overlayHintFallbackRow,
|
|
1249
|
+
overlayHintOnLastItem,
|
|
1250
|
+
panelInkMaskEpoch,
|
|
1251
|
+
panelTransitionEpoch,
|
|
1252
|
+
picker,
|
|
1253
|
+
pickerOpenedFromEnterRef,
|
|
1254
|
+
pickerOpenedFromEnterTimerRef,
|
|
1255
|
+
promptBoxRectRef,
|
|
1256
|
+
promptDraft,
|
|
1257
|
+
promptDraftOverride,
|
|
1258
|
+
promptMouseSelectionRef,
|
|
1259
|
+
promptSelectionRef,
|
|
1260
|
+
promptValueRef,
|
|
1261
|
+
providerPrompt,
|
|
1262
|
+
renderedTranscriptItems,
|
|
1263
|
+
resizeEpoch,
|
|
1264
|
+
resizeState,
|
|
1265
|
+
restoreQueuedToPrompt,
|
|
1266
|
+
setPicker,
|
|
1267
|
+
setSlashIndex,
|
|
1268
|
+
setTextEntryLayoutRows,
|
|
1269
|
+
settingsPrompt,
|
|
1270
|
+
slashCommands,
|
|
1271
|
+
slashIndex,
|
|
1272
|
+
slashPaletteOpen,
|
|
1273
|
+
state,
|
|
1274
|
+
statuslineStats,
|
|
1275
|
+
store,
|
|
1276
|
+
toolApproval,
|
|
1277
|
+
toolOutputExpanded,
|
|
1278
|
+
transcriptMeasureRef,
|
|
1279
|
+
transcriptTailPinned,
|
|
1280
|
+
transcriptWindow,
|
|
1281
|
+
tuiReady,
|
|
1282
|
+
usagePanel,
|
|
1283
|
+
});
|
|
3498
1284
|
}
|