mixdog 0.9.4 → 0.9.6
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/README.md +112 -38
- package/package.json +4 -1
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench-run.mjs +40 -7
- package/scripts/internal-comms-bench.mjs +2 -4
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/tool-smoke.mjs +2 -2
- package/src/agents/heavy-worker/AGENT.md +7 -7
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +9 -10
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/mixdog-session-runtime.mjs +15 -14
- package/src/output-styles/default.md +14 -18
- package/src/output-styles/minimal.md +4 -5
- package/src/output-styles/simple.md +7 -8
- package/src/rules/agent/00-common.md +6 -5
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +6 -3
- package/src/rules/lead/lead-tool.md +6 -4
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +15 -26
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +23 -43
- package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
- package/src/runtime/channels/lib/event-queue.mjs +15 -9
- package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +8 -47
- package/src/runtime/memory/index.mjs +9 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/query-handlers.mjs +27 -5
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/session-runtime/config-helpers.mjs +45 -10
- package/src/session-runtime/quick-model-rows.mjs +46 -11
- package/src/session-runtime/quick-search-models.mjs +28 -27
- package/src/session-runtime/settings-api.mjs +34 -1
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool.mjs +30 -0
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/explore-tool.mjs +39 -10
- package/src/tui/App.jsx +131 -28
- package/src/tui/app/app-format.mjs +11 -4
- package/src/tui/app/channel-pickers.mjs +2 -4
- package/src/tui/app/maintenance-pickers.mjs +60 -28
- package/src/tui/app/model-options.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +0 -1
- package/src/tui/app/transcript-window.mjs +8 -2
- package/src/tui/app/use-transcript-scroll.mjs +3 -1
- package/src/tui/app/use-transcript-window.mjs +19 -1
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +32 -11
- package/src/tui/components/PromptInput.jsx +10 -1
- package/src/tui/components/StatusLine.jsx +6 -3
- package/src/tui/dist/index.mjs +441 -293
- package/src/tui/engine.mjs +48 -12
- package/src/tui/index.jsx +11 -52
- package/src/ui/statusline.mjs +10 -0
- package/src/workflows/default/WORKFLOW.md +15 -17
- package/src/workflows/sequential/WORKFLOW.md +15 -17
- package/vendor/ink/build/ink.js +9 -1
- package/scripts/recall-usecase-probe.json +0 -6
- package/src/runtime/channels/lib/holidays.mjs +0 -138
package/src/tui/App.jsx
CHANGED
|
@@ -218,6 +218,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
218
218
|
const [tuiReady, setTuiReady] = useState(false);
|
|
219
219
|
const exitRequestedRef = useRef(false);
|
|
220
220
|
const [resizeState, setResizeState] = useState(() => ({ ...terminalSize(stdout), epoch: 0 }));
|
|
221
|
+
const [panelTransitionEpoch, setPanelTransitionEpoch] = useState(0);
|
|
221
222
|
// Windows Terminal/conhost scrolls the alt-screen (auto-wrap/DECAWM) when the
|
|
222
223
|
// bottom-right cell is written. WT_SESSION is also set when the UI runs under
|
|
223
224
|
// a Unix-ish shell hosted by Windows Terminal, where process.platform is not
|
|
@@ -477,6 +478,11 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
477
478
|
const [slashIndex, setSlashIndex] = useState(0);
|
|
478
479
|
const [slashDismissedFor, setSlashDismissedFor] = useState('');
|
|
479
480
|
const slashPaletteRef = useRef({ open: false, count: 0 });
|
|
481
|
+
// Holding Tab can generate key-repeat faster than a workflow switch can
|
|
482
|
+
// settle. Without a prompt-local guard every repeat starts (or rejects) an
|
|
483
|
+
// async switch and pushes a toast, producing a rapid bottom-layout repaint
|
|
484
|
+
// storm that can visually tear the prompt box in Windows Terminal.
|
|
485
|
+
const workflowTabCycleRef = useRef({ pending: false, lastAt: 0 });
|
|
480
486
|
const scrollFocusRef = useRef({});
|
|
481
487
|
const onboardingStartedRef = useRef(false);
|
|
482
488
|
const onboardingRef = useRef({ defaultRoute: null, searchRoute: null, agentRoutes: {}, agents: [], providerModels: [] });
|
|
@@ -671,6 +677,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
671
677
|
// extendSelection). Null ⇔ ordinary char-drag selection.
|
|
672
678
|
const dragRef = useRef({ anchor: null, anchorScroll: 0, last: null, active: false, rect: null, region: null, anchorSpan: null });
|
|
673
679
|
const transcriptViewportRef = useRef({ top: 0, bottom: 0 });
|
|
680
|
+
const panelTransitionRef = useRef({ signature: '', reserve: 0, clearRows: 0, guardRows: 0, epoch: 0 });
|
|
674
681
|
// [mixdog] Latest terminal row count + the statusline band (bottom rows),
|
|
675
682
|
// refreshed each render. The mouse handler uses these to (a) clip a status-bar
|
|
676
683
|
// grid selection to the statusline rows and (b) route a press to the right
|
|
@@ -1249,10 +1256,22 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1249
1256
|
toggleExpand();
|
|
1250
1257
|
return;
|
|
1251
1258
|
}
|
|
1259
|
+
const rawShiftUp = input === '\x1b[1;2A' || input === '\x1b[a' || input === '[1;2A';
|
|
1260
|
+
const rawShiftDown = input === '\x1b[1;2B' || input === '\x1b[b' || input === '[1;2B';
|
|
1261
|
+
const rawShiftRight = input === '\x1b[1;2C' || input === '\x1b[c' || input === '[1;2C';
|
|
1262
|
+
const rawShiftLeft = input === '\x1b[1;2D' || input === '\x1b[d' || input === '[1;2D';
|
|
1263
|
+
const rawCtrlShiftUp = input === '\x1b[1;6A' || input === '[1;6A';
|
|
1264
|
+
const rawCtrlShiftDown = input === '\x1b[1;6B' || input === '[1;6B';
|
|
1265
|
+
const rawCtrlShiftRight = input === '\x1b[1;6C' || input === '[1;6C';
|
|
1266
|
+
const rawCtrlShiftLeft = input === '\x1b[1;6D' || input === '[1;6D';
|
|
1267
|
+
const rawModifiedShiftArrow = rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
|
|
1268
|
+
|| rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
|
|
1252
1269
|
if (
|
|
1253
1270
|
!picker
|
|
1254
|
-
&&
|
|
1255
|
-
|
|
1271
|
+
&& (
|
|
1272
|
+
(key.shift && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end))
|
|
1273
|
+
|| rawModifiedShiftArrow
|
|
1274
|
+
)
|
|
1256
1275
|
) {
|
|
1257
1276
|
// Consume the chord whenever a transcript/status ink-grid selection is
|
|
1258
1277
|
// live — even if the focus clamps at an edge (moveSelectionFocus returns
|
|
@@ -1260,10 +1279,10 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1260
1279
|
// shared gridSelectionActiveRef predicate, so there is no double-handling.
|
|
1261
1280
|
// When no grid selection is live, fall through to PromptInput.
|
|
1262
1281
|
let move = null;
|
|
1263
|
-
if (key.leftArrow) move = 'left';
|
|
1264
|
-
else if (key.rightArrow) move = 'right';
|
|
1265
|
-
else if (key.upArrow) move = 'up';
|
|
1266
|
-
else if (key.downArrow) move = 'down';
|
|
1282
|
+
if (key.leftArrow || rawShiftLeft || rawCtrlShiftLeft) move = 'left';
|
|
1283
|
+
else if (key.rightArrow || rawShiftRight || rawCtrlShiftRight) move = 'right';
|
|
1284
|
+
else if (key.upArrow || rawShiftUp || rawCtrlShiftUp) move = 'up';
|
|
1285
|
+
else if (key.downArrow || rawShiftDown || rawCtrlShiftDown) move = 'down';
|
|
1267
1286
|
else if (key.home) move = 'lineStart';
|
|
1268
1287
|
else if (key.end) move = 'lineEnd';
|
|
1269
1288
|
if (move && gridSelectionActiveRef.current()) {
|
|
@@ -1733,21 +1752,13 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1733
1752
|
}
|
|
1734
1753
|
const parts = commandText.split('|').map((part) => part.trim());
|
|
1735
1754
|
if (channelPrompt.kind === 'channel-add') {
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
// so
|
|
1739
|
-
const [first, pipeId, mode, mainFlag] = parts;
|
|
1755
|
+
// Single-channel: the UI asks only for the channel id. Legacy
|
|
1756
|
+
// `name | id | ...` pipe input still parses (the id is the second
|
|
1757
|
+
// field) so old muscle memory does not break.
|
|
1740
1758
|
const isPipe = parts.length > 1;
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
const main = !isPipe
|
|
1744
|
-
|| String(mainFlag || '').toLowerCase() === 'main'
|
|
1745
|
-
|| String(name || '').toLowerCase() === 'main';
|
|
1746
|
-
store.saveChannel({
|
|
1747
|
-
name,
|
|
1759
|
+
const channelId = isPipe ? parts[1] : parts[0];
|
|
1760
|
+
store.setChannel({
|
|
1748
1761
|
channelId,
|
|
1749
|
-
mode: isPipe ? mode : 'interactive',
|
|
1750
|
-
main,
|
|
1751
1762
|
backend: channelPrompt.backend,
|
|
1752
1763
|
});
|
|
1753
1764
|
resumeAfterChannelPrompt(channelPrompt);
|
|
@@ -1876,6 +1887,25 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1876
1887
|
void openSettingsPicker();
|
|
1877
1888
|
return true;
|
|
1878
1889
|
}
|
|
1890
|
+
if (settingsPrompt.kind === 'autoclear-provider') {
|
|
1891
|
+
const provider = String(settingsPrompt.provider || '').trim();
|
|
1892
|
+
if (!provider) {
|
|
1893
|
+
store.pushNotice('auto-clear provider is missing', 'warn');
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
const text = String(commandText || '').trim();
|
|
1897
|
+
try {
|
|
1898
|
+
if (text) store.setAutoClear?.({ provider, duration: text });
|
|
1899
|
+
else store.setAutoClear?.({ provider, resetProvider: true });
|
|
1900
|
+
store.pushNotice(text ? `Auto-clear ${provider} default set to ${text}` : `Auto-clear ${provider} default reset`, 'info');
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
store.pushNotice(`autoclear failed: ${e?.message || e}`, 'error');
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
setSettingsPrompt(null);
|
|
1906
|
+
openAutoClearPicker({ advanced: true, returnTo: settingsPrompt.returnTo });
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1879
1909
|
if (settingsPrompt.kind === 'profile-title') {
|
|
1880
1910
|
try {
|
|
1881
1911
|
store.setProfile?.({ title: commandText });
|
|
@@ -2165,6 +2195,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2165
2195
|
openProjectPicker();
|
|
2166
2196
|
} else if (kind === 'core-add' || kind === 'core-edit' || kind === 'core-delete-confirm') {
|
|
2167
2197
|
openMemoryCorePicker();
|
|
2198
|
+
} else if (kind === 'autoclear-provider') {
|
|
2199
|
+
openAutoClearPicker({ advanced: true, returnTo: settingsPrompt?.returnTo });
|
|
2168
2200
|
}
|
|
2169
2201
|
}, [settingsPrompt, showPromptHint]);
|
|
2170
2202
|
|
|
@@ -2345,6 +2377,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2345
2377
|
const hasTextEntryPrompt = !!textEntryPrompt;
|
|
2346
2378
|
const hasFloatingPanel = !!(toolApproval || picker || contextPanel || usagePanel || slashPaletteOpen || hasTextEntryPrompt);
|
|
2347
2379
|
const expandedOptionPanel = !!(toolApproval || picker || contextPanel || usagePanel || hasTextEntryPrompt);
|
|
2380
|
+
const bootSettling = !tuiReady && state.items.length === 0 && !hasFloatingPanel;
|
|
2348
2381
|
// Project selection (initial-entry experience) keeps the welcome banner
|
|
2349
2382
|
// visible above the picker / path-entry prompt, unlike other floating panels.
|
|
2350
2383
|
const projectSelectionActive = picker?.kind === 'project'
|
|
@@ -2353,10 +2386,15 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2353
2386
|
|| settingsPrompt?.kind === 'project-rename';
|
|
2354
2387
|
// Slash search floats above the normal prompt. Actual option panels own the
|
|
2355
2388
|
// prompt/status area, so they hide those rows and expand into that space.
|
|
2356
|
-
const inputBoxHidden = expandedOptionPanel;
|
|
2389
|
+
const inputBoxHidden = expandedOptionPanel || bootSettling;
|
|
2357
2390
|
const showWelcomeBanner = (state.items.length === 0 && !hasFloatingPanel) || projectSelectionActive || onboardingActive;
|
|
2358
2391
|
const WELCOME_ROWS = showWelcomeBanner ? 11 : 0;
|
|
2359
2392
|
const liveSpinner = state.spinner?.active ? state.spinner : (state.commandStatus?.active ? state.commandStatus : null);
|
|
2393
|
+
// Command-status spinner (auto-clear/compact/etc.) is NOT part of the
|
|
2394
|
+
// spinner → TurnDone handoff: it typically starts while the transcript tail
|
|
2395
|
+
// is already a done row (idle session), so the done-at-tail suppression
|
|
2396
|
+
// below must never hide it — that read as a frozen UI during auto-clear.
|
|
2397
|
+
const liveSpinnerIsCommand = !state.spinner?.active && !!state.commandStatus?.active;
|
|
2360
2398
|
const latestToast = state.toasts?.length ? state.toasts[state.toasts.length - 1] : null;
|
|
2361
2399
|
const toastHint = latestToast ? latestToast.text : '';
|
|
2362
2400
|
const progressHint = state.progressHint || null;
|
|
@@ -2464,14 +2502,48 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2464
2502
|
const pickerVisibleRows = picker
|
|
2465
2503
|
? Math.max(1, floatingPanelRows - PICKER_CHROME_ROWS)
|
|
2466
2504
|
: PANEL_MAX_VISIBLE;
|
|
2467
|
-
const
|
|
2505
|
+
const rawBottomReserve = baseReserve + floatingPanelRows;
|
|
2506
|
+
const bottomClusterRows = INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows + floatingPanelRows;
|
|
2507
|
+
const panelLayoutSignature = [
|
|
2508
|
+
toolApproval ? 'tool' : '',
|
|
2509
|
+
picker ? `picker:${picker.kind || ''}:${picker.fillAvailable ? 'fill' : 'fit'}` : '',
|
|
2510
|
+
contextPanel ? 'context' : '',
|
|
2511
|
+
usagePanel ? 'usage' : '',
|
|
2512
|
+
slashPaletteOpen ? 'slash' : '',
|
|
2513
|
+
hasTextEntryPrompt ? `text:${textEntryPrompt?.kind || ''}` : '',
|
|
2514
|
+
inputBoxHidden ? 'input-hidden' : 'input-visible',
|
|
2515
|
+
floatingPanelRows,
|
|
2516
|
+
promptBoxRows,
|
|
2517
|
+
promptMetaRows,
|
|
2518
|
+
queuedRows,
|
|
2519
|
+
WELCOME_ROWS,
|
|
2520
|
+
].join('|');
|
|
2521
|
+
const panelTransition = panelTransitionRef.current;
|
|
2522
|
+
const panelLayoutChanged = Boolean(panelTransition.signature && panelTransition.signature !== panelLayoutSignature);
|
|
2523
|
+
let panelTransitionClearRows = 0;
|
|
2524
|
+
let panelTransitionGuardRows = 0;
|
|
2525
|
+
if (panelLayoutChanged) {
|
|
2526
|
+
// When a tall floating panel closes, Ink may expand the transcript into rows
|
|
2527
|
+
// that still contain panel ink from the previous frame. Keep those rows blank
|
|
2528
|
+
// for exactly one commit, then let the transcript claim them on the next tick.
|
|
2529
|
+
panelTransitionClearRows = Math.max(0, panelTransition.reserve - bottomClusterRows);
|
|
2530
|
+
panelTransitionGuardRows = panelTransitionClearRows > 0 ? 1 : 0;
|
|
2531
|
+
panelTransition.clearRows = panelTransitionClearRows;
|
|
2532
|
+
panelTransition.guardRows = panelTransitionGuardRows;
|
|
2533
|
+
panelTransition.epoch = panelTransitionEpoch;
|
|
2534
|
+
} else if (panelTransition.epoch === panelTransitionEpoch) {
|
|
2535
|
+
panelTransitionClearRows = panelTransition.clearRows || 0;
|
|
2536
|
+
panelTransitionGuardRows = panelTransition.guardRows || 0;
|
|
2537
|
+
}
|
|
2538
|
+
const bottomReserve = rawBottomReserve + panelTransitionClearRows;
|
|
2468
2539
|
const viewportHeight = Math.max(1, resizeState.rows - bottomReserve);
|
|
2469
2540
|
// Keep one physical row between the transcript clip and the bottom cluster
|
|
2470
2541
|
// even when pinned to the live tail. Windows Terminal/conhost can still
|
|
2471
2542
|
// surface one clipped/off-by-one transcript row below the statusline during
|
|
2472
2543
|
// rapid tool-card updates; a permanent guard row makes that row blank instead
|
|
2473
2544
|
// of a tool header/detail.
|
|
2474
|
-
const
|
|
2545
|
+
const guardCapacityRows = Math.max(0, viewportHeight - 1);
|
|
2546
|
+
const baseGuardRows = guardCapacityRows > 0 ? 1 : 0;
|
|
2475
2547
|
// ── Scroll-time overprint guard ───────────────────────────────────────────
|
|
2476
2548
|
// Wheel/manual scroll pushes the transcript column DOWN via a negative
|
|
2477
2549
|
// marginBottom (see the viewport render). Under conhost/Windows Terminal the
|
|
@@ -2483,7 +2555,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2483
2555
|
// viewport is genuinely scrolled up. The extra blank row absorbs the stray
|
|
2484
2556
|
// paint instead of the statusline. Requires a viewport tall enough to spare
|
|
2485
2557
|
// the row, and never shrinks below the base guard.
|
|
2486
|
-
const transcriptGuardRows = baseGuardRows;
|
|
2558
|
+
const transcriptGuardRows = Math.min(guardCapacityRows, baseGuardRows + panelTransitionGuardRows);
|
|
2487
2559
|
const transcriptContentHeight = Math.max(1, viewportHeight - transcriptGuardRows);
|
|
2488
2560
|
// Bottom-follow / pin semantics must NOT widen with the scroll-time guard, or
|
|
2489
2561
|
// the "pinned to tail" threshold would drift and streaming could freeze a row
|
|
@@ -2538,6 +2610,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2538
2610
|
&& !inputHint
|
|
2539
2611
|
);
|
|
2540
2612
|
welcomePromptHintVisibleRef.current = welcomePromptHintVisible;
|
|
2613
|
+
useEffect(() => {
|
|
2614
|
+
const transition = panelTransitionRef.current;
|
|
2615
|
+
const hadTransitionClearance = panelTransitionClearRows > 0 || panelTransitionGuardRows > 0;
|
|
2616
|
+
transition.signature = panelLayoutSignature;
|
|
2617
|
+
transition.reserve = bottomClusterRows;
|
|
2618
|
+
transition.clearRows = 0;
|
|
2619
|
+
transition.guardRows = 0;
|
|
2620
|
+
if (!hadTransitionClearance) return undefined;
|
|
2621
|
+
const timer = setTimeout(() => setPanelTransitionEpoch((epoch) => epoch + 1), 0);
|
|
2622
|
+
return () => clearTimeout(timer);
|
|
2623
|
+
}, [panelLayoutSignature, bottomClusterRows, panelTransitionClearRows, panelTransitionGuardRows]);
|
|
2541
2624
|
// Row-index/window memo chain + measured-height harvest + anchor lock:
|
|
2542
2625
|
// extracted to app/use-transcript-window.mjs.
|
|
2543
2626
|
const {
|
|
@@ -2578,6 +2661,14 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2578
2661
|
});
|
|
2579
2662
|
const cycleWorkflowFromPrompt = useCallback(() => {
|
|
2580
2663
|
if (slashPaletteOpen || toolApproval || picker || settingsPrompt || providerPrompt || channelPrompt || hookPrompt || contextPanel || usagePanel) return true;
|
|
2664
|
+
const repeatGuardMs = 300;
|
|
2665
|
+
const cycleGuard = workflowTabCycleRef.current;
|
|
2666
|
+
const now = Date.now();
|
|
2667
|
+
if (state.commandBusy || cycleGuard.pending || now - cycleGuard.lastAt < repeatGuardMs) {
|
|
2668
|
+
cycleGuard.lastAt = now;
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2671
|
+
cycleGuard.lastAt = now;
|
|
2581
2672
|
let workflows = [];
|
|
2582
2673
|
try {
|
|
2583
2674
|
workflows = store.listWorkflows?.() || [];
|
|
@@ -2597,17 +2688,22 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2597
2688
|
const activeIndex = workflows.findIndex((item) => item.active);
|
|
2598
2689
|
const currentIndex = activeIndex >= 0 ? activeIndex : Math.max(0, workflows.findIndex((item) => item.id === workflow.id));
|
|
2599
2690
|
const next = workflows[(currentIndex + 1 + workflows.length) % workflows.length];
|
|
2600
|
-
|
|
2691
|
+
cycleGuard.pending = true;
|
|
2692
|
+
void Promise.resolve()
|
|
2693
|
+
.then(() => store.setWorkflow?.(next.id))
|
|
2601
2694
|
.then((result) => {
|
|
2602
2695
|
if (!result) {
|
|
2603
|
-
store.pushNotice('Workflow switch is already running.', 'warn');
|
|
2604
2696
|
return;
|
|
2605
2697
|
}
|
|
2606
|
-
store.pushNotice(workflowSwitchNotice(result), 'info');
|
|
2698
|
+
store.pushNotice(workflowSwitchNotice(result), 'info', { ttlMs: 1200 });
|
|
2607
2699
|
})
|
|
2608
|
-
.catch((e) => store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error'))
|
|
2700
|
+
.catch((e) => store.pushNotice(`Couldn’t switch workflow: ${e?.message || e}`, 'error'))
|
|
2701
|
+
.finally(() => {
|
|
2702
|
+
cycleGuard.pending = false;
|
|
2703
|
+
cycleGuard.lastAt = Date.now();
|
|
2704
|
+
});
|
|
2609
2705
|
return true;
|
|
2610
|
-
}, [slashPaletteOpen, toolApproval, picker, settingsPrompt, providerPrompt, channelPrompt, hookPrompt, contextPanel, usagePanel, state.workflow, store]);
|
|
2706
|
+
}, [slashPaletteOpen, toolApproval, picker, settingsPrompt, providerPrompt, channelPrompt, hookPrompt, contextPanel, usagePanel, state.commandBusy, state.workflow, store]);
|
|
2611
2707
|
// The hardware/IME caret is parked by PromptInput from its OWN measured box
|
|
2612
2708
|
// position (ink useCursor + useBoxMetrics) — correct now that the transcript
|
|
2613
2709
|
// is a live column, so the live-frame line count ink relies on is accurate.
|
|
@@ -2783,6 +2879,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2783
2879
|
can move; overflow is clipped from the top while the panel remains
|
|
2784
2880
|
bottom-aligned against the prompt. */}
|
|
2785
2881
|
<Box flexDirection="column" flexShrink={0} width="100%" backgroundColor={surfaceBackground()}>
|
|
2882
|
+
{panelTransitionClearRows > 0 ? (
|
|
2883
|
+
<Box height={panelTransitionClearRows} flexShrink={0} width="100%" overflow="hidden" backgroundColor={surfaceBackground()} />
|
|
2884
|
+
) : null}
|
|
2786
2885
|
{floatingPanelRows > 0 ? (
|
|
2787
2886
|
<Box flexDirection="column" flexShrink={0} height={floatingPanelRows} overflow="hidden" justifyContent="flex-end" backgroundColor={surfaceBackground()}>
|
|
2788
2887
|
{toolApproval ? (
|
|
@@ -2948,6 +3047,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2948
3047
|
initialValue={settingsPrompt.initialValue || ''}
|
|
2949
3048
|
actionLabel={settingsPrompt.kind === 'skill-use'
|
|
2950
3049
|
? 'run'
|
|
3050
|
+
: settingsPrompt.kind === 'autoclear-provider'
|
|
3051
|
+
? 'save'
|
|
2951
3052
|
: settingsPrompt.kind === 'project-new'
|
|
2952
3053
|
? 'open'
|
|
2953
3054
|
: settingsPrompt.kind === 'project-create-confirm'
|
|
@@ -2963,6 +3064,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2963
3064
|
: 'save'}
|
|
2964
3065
|
promptLabel={settingsPrompt.kind === 'skill-use'
|
|
2965
3066
|
? 'Command > '
|
|
3067
|
+
: settingsPrompt.kind === 'autoclear-provider'
|
|
3068
|
+
? 'Duration > '
|
|
2966
3069
|
: settingsPrompt.kind === 'project-new'
|
|
2967
3070
|
? 'Path > '
|
|
2968
3071
|
: settingsPrompt.kind === 'project-create-confirm'
|
|
@@ -45,11 +45,11 @@ export function workflowDisplayName(workflow = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function workflowSwitchNotice(workflow = {}) {
|
|
48
|
-
return 'Workflow
|
|
48
|
+
return 'Workflow saved · new sessions';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function modelSwitchNotice() {
|
|
52
|
-
return 'Model
|
|
52
|
+
return 'Model saved · new sessions';
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function compactJson(value, max = 180) {
|
|
@@ -122,13 +122,20 @@ export function formatSessionMessageCount(count) {
|
|
|
122
122
|
export function fitLine(value, columns, reserve = 4) {
|
|
123
123
|
const text = String(value || '');
|
|
124
124
|
const width = Math.max(1, Number(columns || 80) - reserve);
|
|
125
|
-
|
|
125
|
+
if (stringWidth(text) <= width) return text;
|
|
126
|
+
if (width <= 1) return '…'.repeat(Math.max(0, width));
|
|
127
|
+
let out = '';
|
|
128
|
+
for (const ch of text) {
|
|
129
|
+
if (stringWidth(`${out}${ch}…`) > width) break;
|
|
130
|
+
out += ch;
|
|
131
|
+
}
|
|
132
|
+
return `${out}…`;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
export function centerLine(value, columns, reserve = 0) {
|
|
129
136
|
const text = fitLine(value, columns, reserve);
|
|
130
137
|
const width = Math.max(1, Number(columns || 80) - reserve);
|
|
131
|
-
const pad = Math.max(0, Math.floor((width - text
|
|
138
|
+
const pad = Math.max(0, Math.floor((width - stringWidth(text)) / 2));
|
|
132
139
|
return `${' '.repeat(pad)}${text}`;
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -40,8 +40,7 @@ export function createChannelPickers({
|
|
|
40
40
|
const tokenDescription = isTelegram
|
|
41
41
|
? `${setup.telegram?.status ?? 'Off'}${setup.telegram?.problem ? ' · Invalid' : ''}`
|
|
42
42
|
: `${setup.discord.status}${setup.discord.problem ? ' · Invalid' : ''}`;
|
|
43
|
-
const mainEntry =
|
|
44
|
-
|| (setup.channels || []).find((ch) => ch.name === 'main');
|
|
43
|
+
const mainEntry = setup.channel || {};
|
|
45
44
|
const mainTarget = isTelegram
|
|
46
45
|
? (mainEntry?.telegramChatId || (activeBackend === 'telegram' ? mainEntry?.channelId : ''))
|
|
47
46
|
: (mainEntry?.discordChannelId || (activeBackend === 'discord' ? mainEntry?.channelId : ''));
|
|
@@ -133,8 +132,7 @@ export function createChannelPickers({
|
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
134
|
const activeBackend = setup.backend || 'discord';
|
|
136
|
-
const mainEntry =
|
|
137
|
-
|| (setup.channels || []).find((ch) => ch.name === 'main');
|
|
135
|
+
const mainEntry = setup.channel || {};
|
|
138
136
|
const typeDescription = (backend) => {
|
|
139
137
|
const selected = activeBackend === backend;
|
|
140
138
|
const hasToken = backend === 'telegram'
|
|
@@ -140,14 +140,13 @@ export function createMaintenancePickers({
|
|
|
140
140
|
// Lead BP4 messages cache TTL follows autoClear (cache-strategy.mjs
|
|
141
141
|
// resolveLeadMessagesTtl): off or idle>=1h -> 1h, shorter idle -> 5m.
|
|
142
142
|
const HOUR_MS = 60 * 60 * 1000;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
];
|
|
143
|
+
const formatDurationInput = (ms) => {
|
|
144
|
+
const value = Math.max(0, Math.round(Number(ms) || 0));
|
|
145
|
+
if (value > 0 && value % HOUR_MS === 0) return `${value / HOUR_MS}h`;
|
|
146
|
+
if (value > 0 && value % 60_000 === 0) return `${value / 60_000}m`;
|
|
147
|
+
if (value > 0 && value % 1000 === 0) return `${value / 1000}s`;
|
|
148
|
+
return `${value}ms`;
|
|
149
|
+
};
|
|
151
150
|
const readCurrent = () => {
|
|
152
151
|
try { return store.getAutoClear?.() || null; } catch { return null; }
|
|
153
152
|
};
|
|
@@ -164,6 +163,49 @@ export function createMaintenancePickers({
|
|
|
164
163
|
}
|
|
165
164
|
render();
|
|
166
165
|
};
|
|
166
|
+
const openProviderDurationEditor = (entry) => {
|
|
167
|
+
if (!entry?.provider) return;
|
|
168
|
+
setPicker(null);
|
|
169
|
+
setSettingsPrompt({
|
|
170
|
+
kind: 'autoclear-provider',
|
|
171
|
+
label: `Auto-clear · ${entry.provider}`,
|
|
172
|
+
hint: `Type a duration like 10m, 1h, or 24h. Empty resets to built-in ${formatDurationInput(entry.builtInMs)}.`,
|
|
173
|
+
initialValue: formatDurationInput(entry.idleMs),
|
|
174
|
+
provider: entry.provider,
|
|
175
|
+
builtInMs: entry.builtInMs,
|
|
176
|
+
returnTo,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const renderAdvanced = () => {
|
|
180
|
+
const current = readCurrent();
|
|
181
|
+
const provider = current?.provider || 'default';
|
|
182
|
+
const providerDefaults = Array.isArray(current?.providerDefaults) ? current.providerDefaults : [];
|
|
183
|
+
const items = providerDefaults.map((entry) => ({
|
|
184
|
+
value: `provider:${entry.provider}`,
|
|
185
|
+
label: entry.provider,
|
|
186
|
+
marker: entry.provider === provider ? '✓' : '',
|
|
187
|
+
markerColor: theme.success,
|
|
188
|
+
meta: `${formatDuration(entry.idleMs)}${entry.custom ? ' custom' : ''}`,
|
|
189
|
+
description: `Default idle window for ${entry.provider}. Enter to edit as text.`,
|
|
190
|
+
_action: 'provider-default',
|
|
191
|
+
_entry: entry,
|
|
192
|
+
}));
|
|
193
|
+
setPicker({
|
|
194
|
+
title: 'Auto-clear · Advanced',
|
|
195
|
+
description: 'Provider default idle windows. Enter edits the duration text.',
|
|
196
|
+
help: '↑/↓ Select · Enter Edit · Esc Back',
|
|
197
|
+
indexMode: 'always',
|
|
198
|
+
labelWidth: 18,
|
|
199
|
+
metaWidth: 18,
|
|
200
|
+
items,
|
|
201
|
+
onSelect: (_value, item) => {
|
|
202
|
+
if (item?._action === 'provider-default') openProviderDurationEditor(item._entry);
|
|
203
|
+
},
|
|
204
|
+
onCancel: () => {
|
|
205
|
+
render();
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
};
|
|
167
209
|
const render = () => {
|
|
168
210
|
const current = readCurrent();
|
|
169
211
|
const enabled = current?.enabled !== false;
|
|
@@ -182,28 +224,19 @@ export function createMaintenancePickers({
|
|
|
182
224
|
_action: 'toggle',
|
|
183
225
|
},
|
|
184
226
|
{
|
|
185
|
-
value: '
|
|
186
|
-
label: '
|
|
227
|
+
value: 'advanced',
|
|
228
|
+
label: 'Advanced',
|
|
187
229
|
marker: !custom ? '✓' : '',
|
|
188
230
|
markerColor: theme.success,
|
|
189
|
-
meta:
|
|
190
|
-
description:
|
|
191
|
-
_action: '
|
|
231
|
+
meta: `${current?.provider || 'current'} · ${formatDuration(providerDefault)}`,
|
|
232
|
+
description: 'Edit provider-paired default idle windows as text.',
|
|
233
|
+
_action: 'advanced',
|
|
192
234
|
},
|
|
193
|
-
...IDLE_WINDOWS.map((win) => ({
|
|
194
|
-
value: win.value,
|
|
195
|
-
label: win.label,
|
|
196
|
-
marker: custom && idleMs === win.ms ? '✓' : '',
|
|
197
|
-
markerColor: theme.success,
|
|
198
|
-
description: `Clear idle sessions after ${win.label}.`,
|
|
199
|
-
_action: 'window',
|
|
200
|
-
_idleMs: win.ms,
|
|
201
|
-
})),
|
|
202
235
|
];
|
|
203
236
|
setPicker({
|
|
204
237
|
title: 'Auto-clear',
|
|
205
238
|
description: `Clear idle context after ${enabled ? formatDuration(idleMs) : 'never'} · lead cache TTL ${cacheTtlLabel}.`,
|
|
206
|
-
help: '↑/↓ Select · ←/→ Toggle On/Off · Enter
|
|
239
|
+
help: '↑/↓ Select · ←/→ Toggle On/Off · Enter Open/Toggle · Esc Close',
|
|
207
240
|
indexMode: 'always',
|
|
208
241
|
labelWidth: 10,
|
|
209
242
|
items,
|
|
@@ -216,10 +249,8 @@ export function createMaintenancePickers({
|
|
|
216
249
|
onSelect: (_value, item) => {
|
|
217
250
|
if (item?._action === 'toggle') {
|
|
218
251
|
applyAutoClear({ enabled: !enabled });
|
|
219
|
-
} else if (item?._action === '
|
|
220
|
-
|
|
221
|
-
} else if (item?._action === 'window') {
|
|
222
|
-
applyAutoClear({ enabled: true, idleMs: item._idleMs });
|
|
252
|
+
} else if (item?._action === 'advanced') {
|
|
253
|
+
renderAdvanced();
|
|
223
254
|
}
|
|
224
255
|
},
|
|
225
256
|
onCancel: () => {
|
|
@@ -234,7 +265,8 @@ export function createMaintenancePickers({
|
|
|
234
265
|
setSettingsPrompt(null);
|
|
235
266
|
setContextPanel(null);
|
|
236
267
|
closeUsagePanel();
|
|
237
|
-
|
|
268
|
+
if (options.advanced === true) renderAdvanced();
|
|
269
|
+
else render();
|
|
238
270
|
};
|
|
239
271
|
|
|
240
272
|
const openProfilePicker = (options = {}) => {
|
|
@@ -90,6 +90,7 @@ export const modelFamily = (m) => {
|
|
|
90
90
|
export const modelContextWindow = (m) => {
|
|
91
91
|
const raw = Number(m?.contextWindow);
|
|
92
92
|
const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
93
|
+
if (n > 0) return n;
|
|
93
94
|
const provider = String(m?.provider || '').toLowerCase();
|
|
94
95
|
const id = String(m?.id || '').toLowerCase();
|
|
95
96
|
const version = parsedModelVersion(id);
|
|
@@ -105,7 +106,10 @@ export const formatContextWindow = (tokens) => {
|
|
|
105
106
|
if (!Number.isFinite(n) || n <= 0) return '';
|
|
106
107
|
if (n >= 1_000_000) {
|
|
107
108
|
const m = n / 1_000_000;
|
|
108
|
-
|
|
109
|
+
const label = Number.isInteger(m)
|
|
110
|
+
? m.toFixed(0)
|
|
111
|
+
: m.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
|
112
|
+
return `${label}M Context`;
|
|
109
113
|
}
|
|
110
114
|
return `${Math.round(n / 1000)}k Context`;
|
|
111
115
|
};
|
|
@@ -441,7 +441,6 @@ export function createSettingsPicker({
|
|
|
441
441
|
else if (item?._action === 'workflow') cycleWorkflow(1);
|
|
442
442
|
},
|
|
443
443
|
onSelect: (_value, item) => {
|
|
444
|
-
setPicker(null);
|
|
445
444
|
if (item._action === 'autoclear') openAutoClearPicker({ returnTo: openSettingsPicker });
|
|
446
445
|
else if (item._action === 'profile') openProfilePicker({ returnTo: openSettingsPicker });
|
|
447
446
|
else if (item._action === 'autocompact') applyCompaction({ auto: !(compaction.auto !== false) });
|
|
@@ -562,14 +562,20 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
|
|
|
562
562
|
return rows;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
export function buildTranscriptRowIndex(items, {
|
|
565
|
+
export function buildTranscriptRowIndex(items, {
|
|
566
|
+
columns = 80,
|
|
567
|
+
toolOutputExpanded = false,
|
|
568
|
+
suppressMeasuredRowHeights = false,
|
|
569
|
+
} = {}) {
|
|
566
570
|
const allItems = Array.isArray(items) ? items : [];
|
|
567
571
|
const rows = new Array(allItems.length);
|
|
568
572
|
const prefixRows = new Array(allItems.length + 1);
|
|
569
573
|
prefixRows[0] = 0;
|
|
570
574
|
for (let i = 0; i < allItems.length; i++) {
|
|
571
575
|
const item = allItems[i];
|
|
572
|
-
const measured =
|
|
576
|
+
const measured = suppressMeasuredRowHeights
|
|
577
|
+
? null
|
|
578
|
+
: measuredTranscriptRows(item, columns, toolOutputExpanded);
|
|
573
579
|
const rowCount = measured != null
|
|
574
580
|
? measured
|
|
575
581
|
: estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded);
|
|
@@ -430,7 +430,9 @@ export function useTranscriptScroll({
|
|
|
430
430
|
// top edge, App.jsx ~3060) instead of clamping the selection in
|
|
431
431
|
// place, then extend the focus onto the newly revealed top row.
|
|
432
432
|
const beforeTarget = scrollTargetRef.current;
|
|
433
|
-
|
|
433
|
+
const slack = Math.max(0, Number(transcriptBottomSlackRowsRef.current) || 0);
|
|
434
|
+
const deltaRows = beforeTarget <= slack ? (slack + 1 - beforeTarget) : 1;
|
|
435
|
+
scrollTranscriptRows(deltaRows);
|
|
434
436
|
if (scrollTargetRef.current !== beforeTarget) {
|
|
435
437
|
scrolledEdge = true;
|
|
436
438
|
// scrollTranscriptRows REPLACES dragRef.current with a shifted
|
|
@@ -122,11 +122,27 @@ export function useTranscriptWindow({
|
|
|
122
122
|
() => transcriptStructureSignature(items, frameColumns, toolOutputExpanded),
|
|
123
123
|
[items, frameColumns, toolOutputExpanded],
|
|
124
124
|
);
|
|
125
|
+
const transcriptStreamingActive = (items || []).some(
|
|
126
|
+
(item) => item?.kind === 'assistant' && item?.streaming,
|
|
127
|
+
);
|
|
128
|
+
const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
129
|
+
const transcriptPinnedForStreaming = followingRef.current
|
|
130
|
+
|| scrolledUpRowsForPin <= transcriptBottomSlackRows;
|
|
131
|
+
const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
|
|
132
|
+
const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
|
|
133
|
+
const hasStreamingReadingAnchor = !!transcriptAnchorRef.current
|
|
134
|
+
|| transcriptAnchorDirtyRef.current;
|
|
135
|
+
const suppressMeasuredRowHeights = transcriptStreamingActive && (
|
|
136
|
+
transcriptPinnedForStreaming
|
|
137
|
+
|| (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor)
|
|
138
|
+
|| (scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming)
|
|
139
|
+
);
|
|
125
140
|
const transcriptRowIndex = useMemo(() => buildTranscriptRowIndex(items, {
|
|
126
141
|
columns: frameColumns,
|
|
127
142
|
toolOutputExpanded,
|
|
143
|
+
suppressMeasuredRowHeights,
|
|
128
144
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig captures the relevant item changes; measuredRowsVersion folds in app-level measured height corrections
|
|
129
|
-
}), [transcriptStructureSig, measuredRowsVersion]);
|
|
145
|
+
}), [transcriptStructureSig, measuredRowsVersion, suppressMeasuredRowHeights]);
|
|
130
146
|
// ── Same-frame anchor lock ───────────────────────────────────────────────
|
|
131
147
|
// While the user reads older transcript (anchor captured, not dirty), resolve
|
|
132
148
|
// the scroll offset that keeps the anchored viewport-top row fixed for THIS
|
|
@@ -304,6 +320,7 @@ export function useTranscriptWindow({
|
|
|
304
320
|
// capture matches what the user saw rather than the stale scrollOffset
|
|
305
321
|
// state. Bottom-relative, matching transcriptRenderWindow's window math.
|
|
306
322
|
renderOffset: Math.max(0, Number(renderScrollOffset) || 0),
|
|
323
|
+
suppressMeasuredRowHeights,
|
|
307
324
|
};
|
|
308
325
|
// The window memo is keyed on a structure signature that intentionally
|
|
309
326
|
// ignores per-character growth of the streaming assistant text, so its
|
|
@@ -356,6 +373,7 @@ export function useTranscriptWindow({
|
|
|
356
373
|
// overhead on a tall transcript. The cached measurements stay authoritative
|
|
357
374
|
// for the row-index math; a single re-measure is forced on release below.
|
|
358
375
|
if (dragRef.current.active) return;
|
|
376
|
+
if (transcriptStreamingActive && transcriptTailPinned) return;
|
|
359
377
|
const els = transcriptItemElsRef.current;
|
|
360
378
|
if (!els || els.size === 0) return;
|
|
361
379
|
const liveItems = transcriptMeasureItemsRef.current;
|
|
@@ -26,20 +26,23 @@ export function ConfirmBar({ buttons = [], focusedIndex = -1 }) {
|
|
|
26
26
|
const list = Array.isArray(buttons) ? buttons.filter(Boolean) : [];
|
|
27
27
|
if (list.length === 0) return null;
|
|
28
28
|
return (
|
|
29
|
-
<Box flexDirection="row"
|
|
29
|
+
<Box flexDirection="row" flexShrink={0} justifyContent="flex-start" alignItems="center">
|
|
30
30
|
{list.map((button, index) => {
|
|
31
31
|
const isFocused = index === focusedIndex;
|
|
32
32
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
33
|
+
<Box
|
|
34
|
+
key={button.value ?? index}
|
|
35
|
+
marginLeft={index > 0 ? 1 : 0}
|
|
36
|
+
paddingX={1}
|
|
37
|
+
backgroundColor={isFocused ? theme.selectionBackground : undefined}
|
|
38
|
+
>
|
|
35
39
|
<Text
|
|
36
|
-
color={isFocused ? theme.selectionText : theme.
|
|
37
|
-
backgroundColor={isFocused ? theme.selectionBackground : undefined}
|
|
40
|
+
color={isFocused ? theme.selectionText : theme.subtle}
|
|
38
41
|
bold={isFocused}
|
|
39
42
|
>
|
|
40
|
-
{` ${button.label} `}
|
|
43
|
+
{`[ ${button.label} ]`}
|
|
41
44
|
</Text>
|
|
42
|
-
</
|
|
45
|
+
</Box>
|
|
43
46
|
);
|
|
44
47
|
})}
|
|
45
48
|
</Box>
|