mixdog 0.9.86 → 0.9.88
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 +14 -8
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +59 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +147 -16
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +144 -10
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +67 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +203 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +73 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +25 -19
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +22 -6
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +52 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +27 -0
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- 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 +298 -494
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +372 -0
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +383 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +438 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +115 -1166
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +47 -3
- package/src/tui/App.jsx +367 -2579
- 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/resume-picker.mjs +7 -2
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- 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-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +9889 -8999
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +97 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +1 -1
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -442
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Pasted-attachment buffers, extracted from App.jsx. Large pasted images and
|
|
2
|
+
// texts fold into [Image #N] / [Pasted text #N +M lines] prompt tokens; the
|
|
3
|
+
// original payloads live here (ref + mirrored state) until submit expands
|
|
4
|
+
// them back. install* rehydrates a queued/restored draft's buffers, the
|
|
5
|
+
// snapshot clears drop exactly the entries a submit consumed, and register*
|
|
6
|
+
// mints the next token for a fresh paste.
|
|
7
|
+
import { useCallback, useRef, useState } from 'react';
|
|
8
|
+
import { formatImageRef, formatPastedTextRef } from '../paste-attachments.mjs';
|
|
9
|
+
|
|
10
|
+
export function usePastedBuffers() {
|
|
11
|
+
const [, setPastedImages] = useState({});
|
|
12
|
+
const pastedImagesRef = useRef({});
|
|
13
|
+
const nextPastedImageIdRef = useRef(1);
|
|
14
|
+
const [, setPastedTexts] = useState({});
|
|
15
|
+
const pastedTextsRef = useRef({});
|
|
16
|
+
const nextPastedTextIdRef = useRef(1);
|
|
17
|
+
|
|
18
|
+
const installPastedImages = useCallback((images, { merge = true } = {}) => {
|
|
19
|
+
if (!images || typeof images !== 'object' || Object.keys(images).length === 0) return;
|
|
20
|
+
const next = merge ? { ...pastedImagesRef.current, ...images } : { ...images };
|
|
21
|
+
pastedImagesRef.current = next;
|
|
22
|
+
const maxId = Object.keys(next)
|
|
23
|
+
.map((id) => Number(id) || 0)
|
|
24
|
+
.reduce((max, id) => Math.max(max, id), 0);
|
|
25
|
+
if (maxId >= nextPastedImageIdRef.current) nextPastedImageIdRef.current = maxId + 1;
|
|
26
|
+
setPastedImages(next);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const clearPastedImagesSnapshot = useCallback((snapshot = null) => {
|
|
30
|
+
if (!snapshot) {
|
|
31
|
+
if (Object.keys(pastedImagesRef.current || {}).length === 0) return;
|
|
32
|
+
pastedImagesRef.current = {};
|
|
33
|
+
setPastedImages({});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
37
|
+
const next = { ...pastedImagesRef.current };
|
|
38
|
+
let changed = false;
|
|
39
|
+
for (const [id, image] of Object.entries(snapshot)) {
|
|
40
|
+
if (next[id] === image) {
|
|
41
|
+
delete next[id];
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!changed) return;
|
|
46
|
+
pastedImagesRef.current = next;
|
|
47
|
+
setPastedImages(next);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const registerPastedImage = useCallback((image) => {
|
|
51
|
+
if (!image || image.type !== 'image' || !image.content) return '';
|
|
52
|
+
const id = nextPastedImageIdRef.current++;
|
|
53
|
+
const entry = { ...image, id };
|
|
54
|
+
pastedImagesRef.current = { ...pastedImagesRef.current, [id]: entry };
|
|
55
|
+
setPastedImages(pastedImagesRef.current);
|
|
56
|
+
return formatImageRef(id);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const installPastedTexts = useCallback((texts, { merge = true } = {}) => {
|
|
60
|
+
if (!texts || typeof texts !== 'object' || Object.keys(texts).length === 0) return;
|
|
61
|
+
const next = merge ? { ...pastedTextsRef.current, ...texts } : { ...texts };
|
|
62
|
+
pastedTextsRef.current = next;
|
|
63
|
+
const maxId = Object.keys(next)
|
|
64
|
+
.map((id) => Number(id) || 0)
|
|
65
|
+
.reduce((max, id) => Math.max(max, id), 0);
|
|
66
|
+
if (maxId >= nextPastedTextIdRef.current) nextPastedTextIdRef.current = maxId + 1;
|
|
67
|
+
setPastedTexts(next);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const clearPastedTextsSnapshot = useCallback((snapshot = null) => {
|
|
71
|
+
if (!snapshot) {
|
|
72
|
+
if (Object.keys(pastedTextsRef.current || {}).length === 0) return;
|
|
73
|
+
pastedTextsRef.current = {};
|
|
74
|
+
setPastedTexts({});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
78
|
+
const next = { ...pastedTextsRef.current };
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const [id, text] of Object.entries(snapshot)) {
|
|
81
|
+
if (next[id] === text) {
|
|
82
|
+
delete next[id];
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!changed) return;
|
|
87
|
+
pastedTextsRef.current = next;
|
|
88
|
+
setPastedTexts(next);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const registerPastedText = useCallback((text) => {
|
|
92
|
+
const value = String(text ?? '');
|
|
93
|
+
if (!value) return '';
|
|
94
|
+
const id = nextPastedTextIdRef.current++;
|
|
95
|
+
const entry = { id, text: value };
|
|
96
|
+
pastedTextsRef.current = { ...pastedTextsRef.current, [id]: entry };
|
|
97
|
+
setPastedTexts(pastedTextsRef.current);
|
|
98
|
+
return formatPastedTextRef(id, value);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pastedImagesRef,
|
|
103
|
+
nextPastedImageIdRef,
|
|
104
|
+
pastedTextsRef,
|
|
105
|
+
nextPastedTextIdRef,
|
|
106
|
+
setPastedImages,
|
|
107
|
+
setPastedTexts,
|
|
108
|
+
installPastedImages,
|
|
109
|
+
clearPastedImagesSnapshot,
|
|
110
|
+
registerPastedImage,
|
|
111
|
+
installPastedTexts,
|
|
112
|
+
clearPastedTextsSnapshot,
|
|
113
|
+
registerPastedText,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Prompt draft + slash palette flow, extracted from App.jsx: keystroke-time
|
|
2
|
+
// draft sync (slash-token lift, history-nav reset, argument hints), the four
|
|
3
|
+
// text-entry prompt cancel paths, and accept/complete/cancel for the slash
|
|
4
|
+
// palette.
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { slashQuery, slashArgumentHint, slashCommandTokenForPaletteAccept } from './slash-commands.mjs';
|
|
7
|
+
|
|
8
|
+
export function usePromptDraftFlow({
|
|
9
|
+
dismissWelcomePromptHint,
|
|
10
|
+
syncPromptLayoutRows,
|
|
11
|
+
promptHistoryDraftChangeRef,
|
|
12
|
+
promptHistoryNavRef,
|
|
13
|
+
resetPromptHistoryNav,
|
|
14
|
+
setPromptDraft,
|
|
15
|
+
setPromptDraftOverride,
|
|
16
|
+
showPromptHint,
|
|
17
|
+
clearPromptHint,
|
|
18
|
+
promptHintActiveRef,
|
|
19
|
+
promptHintTimerRef,
|
|
20
|
+
slashDismissedFor,
|
|
21
|
+
setSlashDismissedFor,
|
|
22
|
+
providerPrompt,
|
|
23
|
+
channelPrompt,
|
|
24
|
+
hookPrompt,
|
|
25
|
+
settingsPrompt,
|
|
26
|
+
setProviderPrompt,
|
|
27
|
+
setChannelPrompt,
|
|
28
|
+
setHookPrompt,
|
|
29
|
+
setSettingsPrompt,
|
|
30
|
+
oauthSubmitRef,
|
|
31
|
+
openProjectPicker,
|
|
32
|
+
openMemoryCorePicker,
|
|
33
|
+
openAutoClearPicker,
|
|
34
|
+
slashCommands,
|
|
35
|
+
slashIndex,
|
|
36
|
+
pickerOpenedFromEnterRef,
|
|
37
|
+
pickerOpenedFromEnterTimerRef,
|
|
38
|
+
runSlashCommand,
|
|
39
|
+
}) {
|
|
40
|
+
const onPromptDraftChange = useCallback((value) => {
|
|
41
|
+
if (String(value ?? '').length > 0) dismissWelcomePromptHint();
|
|
42
|
+
syncPromptLayoutRows(value);
|
|
43
|
+
// NOTE: do NOT prune pasted-text entries on edit. A partially-edited token
|
|
44
|
+
// can be undone back to its intact form, which must still expand on submit;
|
|
45
|
+
// entries are kept until an accepted submit or an explicit clear. (Memory
|
|
46
|
+
// cost is bounded and acceptable.)
|
|
47
|
+
const suppressPromptHint = promptHistoryDraftChangeRef.current;
|
|
48
|
+
promptHistoryDraftChangeRef.current = false;
|
|
49
|
+
const historyNav = promptHistoryNavRef.current;
|
|
50
|
+
if (!value || (historyNav.active && value !== historyNav.lastValue && value !== historyNav.seed)) {
|
|
51
|
+
resetPromptHistoryNav();
|
|
52
|
+
}
|
|
53
|
+
// Only lift the draft into App state when it can affect the slash palette
|
|
54
|
+
// (a single "/token"). Prose typing renders entirely inside PromptInput's
|
|
55
|
+
// own state, so App need not re-render — and relayout the full fullscreen
|
|
56
|
+
// frame — on every keystroke (input lag fix). Entering slash mode and
|
|
57
|
+
// leaving it both still sync because either prev or next is a slash token.
|
|
58
|
+
// Clearing/submitting must also sync so a consumed slash command does not
|
|
59
|
+
// remount later as stale initialValue after a picker/panel closes.
|
|
60
|
+
const nextSlash = slashQuery(value);
|
|
61
|
+
setPromptDraft((prev) => {
|
|
62
|
+
const previousWasSlashFlow = String(prev || '').startsWith('/');
|
|
63
|
+
if (value === '') return '';
|
|
64
|
+
return nextSlash !== null || previousWasSlashFlow ? value : prev;
|
|
65
|
+
});
|
|
66
|
+
setPromptDraftOverride((prev) => (prev === null ? prev : null));
|
|
67
|
+
const argumentHint = slashArgumentHint(value);
|
|
68
|
+
if (argumentHint && !suppressPromptHint) {
|
|
69
|
+
showPromptHint(argumentHint, 'info');
|
|
70
|
+
} else if (suppressPromptHint || promptHintActiveRef.current || promptHintTimerRef.current) {
|
|
71
|
+
// Only clear when a hint is actually live (shown or pending its timer).
|
|
72
|
+
// clearPromptHint() already early-returns when neither ref is set, but
|
|
73
|
+
// gating the call here avoids invoking it on EVERY keystroke once a hint
|
|
74
|
+
// has appeared — that call path otherwise drives a setState → full App
|
|
75
|
+
// re-render per key, which is costly on long transcripts. Hint-while-
|
|
76
|
+
// typing still vanishes immediately because the guard includes the active
|
|
77
|
+
// state; the argumentHint branch above is untouched. The guard no longer
|
|
78
|
+
// requires a non-empty value: clearing/submitting to '' must also dismiss
|
|
79
|
+
// a live hint instead of leaving it until its timer expires.
|
|
80
|
+
clearPromptHint();
|
|
81
|
+
}
|
|
82
|
+
if (slashDismissedFor) {
|
|
83
|
+
setSlashDismissedFor((dismissed) => (dismissed && dismissed !== value ? '' : dismissed));
|
|
84
|
+
}
|
|
85
|
+
}, [clearPromptHint, dismissWelcomePromptHint, resetPromptHistoryNav, showPromptHint, slashDismissedFor, syncPromptLayoutRows]);
|
|
86
|
+
|
|
87
|
+
const cancelProviderPrompt = useCallback(() => {
|
|
88
|
+
try { providerPrompt?.login?.cancel?.(); } catch {}
|
|
89
|
+
oauthSubmitRef.current = false;
|
|
90
|
+
const onCancel = providerPrompt?.cancelReturn || providerPrompt?.onCancel;
|
|
91
|
+
const afterSave = providerPrompt?.afterSave;
|
|
92
|
+
setProviderPrompt(null);
|
|
93
|
+
if (onCancel) onCancel();
|
|
94
|
+
else if (afterSave) afterSave();
|
|
95
|
+
}, [providerPrompt, showPromptHint]);
|
|
96
|
+
|
|
97
|
+
const cancelChannelPrompt = useCallback(() => {
|
|
98
|
+
const onCancel = channelPrompt?.onCancel;
|
|
99
|
+
const afterSave = channelPrompt?.afterSave;
|
|
100
|
+
setChannelPrompt(null);
|
|
101
|
+
if (typeof onCancel === 'function') onCancel();
|
|
102
|
+
else if (typeof afterSave === 'function') afterSave();
|
|
103
|
+
}, [channelPrompt, showPromptHint]);
|
|
104
|
+
|
|
105
|
+
const cancelHookPrompt = useCallback(() => {
|
|
106
|
+
setHookPrompt(null);
|
|
107
|
+
}, [showPromptHint]);
|
|
108
|
+
|
|
109
|
+
const cancelSettingsPrompt = useCallback(() => {
|
|
110
|
+
// The project entry prompts are reached from the project picker; backing out
|
|
111
|
+
// (Esc) should return to that picker rather than dropping to a bare prompt.
|
|
112
|
+
const kind = settingsPrompt?.kind;
|
|
113
|
+
setSettingsPrompt(null);
|
|
114
|
+
if (kind === 'project-new' || kind === 'project-create-confirm' || kind === 'project-rename') {
|
|
115
|
+
openProjectPicker();
|
|
116
|
+
} else if (kind === 'core-add' || kind === 'core-edit' || kind === 'core-delete-confirm') {
|
|
117
|
+
openMemoryCorePicker();
|
|
118
|
+
} else if (kind === 'autoclear-provider') {
|
|
119
|
+
openAutoClearPicker({ advanced: true, returnTo: settingsPrompt?.returnTo });
|
|
120
|
+
}
|
|
121
|
+
}, [settingsPrompt, showPromptHint]);
|
|
122
|
+
|
|
123
|
+
const acceptSlashPalette = useCallback((draftValue = '') => {
|
|
124
|
+
const command = slashCommands[slashIndex];
|
|
125
|
+
if (!command) return false;
|
|
126
|
+
pickerOpenedFromEnterRef.current = true;
|
|
127
|
+
if (pickerOpenedFromEnterTimerRef.current) {
|
|
128
|
+
clearTimeout(pickerOpenedFromEnterTimerRef.current);
|
|
129
|
+
pickerOpenedFromEnterTimerRef.current = null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return runSlashCommand(slashCommandTokenForPaletteAccept(command, draftValue), '');
|
|
133
|
+
} finally {
|
|
134
|
+
pickerOpenedFromEnterTimerRef.current = setTimeout(() => {
|
|
135
|
+
pickerOpenedFromEnterRef.current = false;
|
|
136
|
+
pickerOpenedFromEnterTimerRef.current = null;
|
|
137
|
+
}, 3000);
|
|
138
|
+
}
|
|
139
|
+
}, [slashCommands, slashIndex]);
|
|
140
|
+
|
|
141
|
+
const completeSlashPalette = useCallback((draftValue = '') => {
|
|
142
|
+
const command = slashCommands[slashIndex];
|
|
143
|
+
if (!command) return undefined;
|
|
144
|
+
const token = slashCommandTokenForPaletteAccept(command, draftValue);
|
|
145
|
+
return token ? `/${token} ` : undefined;
|
|
146
|
+
}, [slashCommands, slashIndex]);
|
|
147
|
+
|
|
148
|
+
const cancelSlashPalette = useCallback((value = '') => {
|
|
149
|
+
// Esc clears the slash draft, so the dismissal marker must not survive.
|
|
150
|
+
// If it stays as "/" then typing "/" again is treated as the same
|
|
151
|
+
// dismissed query and the palette never re-opens.
|
|
152
|
+
setSlashDismissedFor('');
|
|
153
|
+
setPromptDraft('');
|
|
154
|
+
setPromptDraftOverride({ id: Date.now(), value: '' });
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
onPromptDraftChange,
|
|
159
|
+
cancelProviderPrompt,
|
|
160
|
+
cancelChannelPrompt,
|
|
161
|
+
cancelHookPrompt,
|
|
162
|
+
cancelSettingsPrompt,
|
|
163
|
+
acceptSlashPalette,
|
|
164
|
+
completeSlashPalette,
|
|
165
|
+
cancelSlashPalette,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Prompt-hint band, extracted from App.jsx. One transient hint line under the
|
|
2
|
+
// prompt (copy feedback, queue restore notices, slash argument hints) with a
|
|
3
|
+
// single auto-dismiss timer; the active/timer refs let key handlers decide
|
|
4
|
+
// whether a hint currently owns the band before clearing it.
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
const PROMPT_HINT_DISMISS_MS = 2200;
|
|
8
|
+
|
|
9
|
+
export function usePromptHint() {
|
|
10
|
+
const [promptHint, setPromptHint] = useState('');
|
|
11
|
+
const [promptHintTone, setPromptHintTone] = useState('info');
|
|
12
|
+
const promptHintTimerRef = useRef(null);
|
|
13
|
+
const promptHintActiveRef = useRef(false);
|
|
14
|
+
|
|
15
|
+
const showPromptHint = useCallback((text, tone = 'info') => {
|
|
16
|
+
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
17
|
+
promptHintActiveRef.current = true;
|
|
18
|
+
setPromptHint(String(text || ''));
|
|
19
|
+
setPromptHintTone(tone);
|
|
20
|
+
promptHintTimerRef.current = setTimeout(() => {
|
|
21
|
+
promptHintTimerRef.current = null;
|
|
22
|
+
promptHintActiveRef.current = false;
|
|
23
|
+
setPromptHint('');
|
|
24
|
+
setPromptHintTone('info');
|
|
25
|
+
}, PROMPT_HINT_DISMISS_MS);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Same band and timer; only the resting tone differs for copy feedback.
|
|
29
|
+
const showSelectionCopyHint = useCallback(
|
|
30
|
+
(text, tone = 'plain') => showPromptHint(text, tone),
|
|
31
|
+
[showPromptHint],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const clearPromptHint = useCallback(() => {
|
|
35
|
+
if (!promptHintActiveRef.current && !promptHintTimerRef.current) return;
|
|
36
|
+
if (promptHintTimerRef.current) {
|
|
37
|
+
clearTimeout(promptHintTimerRef.current);
|
|
38
|
+
promptHintTimerRef.current = null;
|
|
39
|
+
}
|
|
40
|
+
promptHintActiveRef.current = false;
|
|
41
|
+
setPromptHint('');
|
|
42
|
+
setPromptHintTone('info');
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => () => {
|
|
46
|
+
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
promptHint,
|
|
51
|
+
promptHintTone,
|
|
52
|
+
promptHintTimerRef,
|
|
53
|
+
promptHintActiveRef,
|
|
54
|
+
showPromptHint,
|
|
55
|
+
showSelectionCopyHint,
|
|
56
|
+
clearPromptHint,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Exit + queued-prompt/history plumbing, extracted from App.jsx: the clean
|
|
2
|
+
// two-phase exit (final frame flush, store dispose race, hard-exit timer),
|
|
3
|
+
// queued-message restore into the draft, the engine-published prompt
|
|
4
|
+
// history with its local-scan fallback, and history-nav reset.
|
|
5
|
+
import { useCallback, useMemo } from 'react';
|
|
6
|
+
import { PROMPT_HISTORY_LIMIT } from './transcript-window.mjs';
|
|
7
|
+
import { promptHistoryKey } from './app-format.mjs';
|
|
8
|
+
|
|
9
|
+
export function usePromptQueueHistory({
|
|
10
|
+
store,
|
|
11
|
+
state,
|
|
12
|
+
exit,
|
|
13
|
+
exitRequestedRef,
|
|
14
|
+
setExiting,
|
|
15
|
+
promptValueRef,
|
|
16
|
+
promptDraft,
|
|
17
|
+
showPromptHint,
|
|
18
|
+
clearPromptHint,
|
|
19
|
+
installPastedImages,
|
|
20
|
+
installPastedTexts,
|
|
21
|
+
syncPromptLayoutRows,
|
|
22
|
+
setPromptDraftOverride,
|
|
23
|
+
promptHistoryNavRef,
|
|
24
|
+
}) {
|
|
25
|
+
// `exiting` removes the inline caret (PromptInput draws none when disabled) and
|
|
26
|
+
// freezes input for the teardown frame, so the final frame is clean before ink
|
|
27
|
+
// unmounts. Exit just past the render throttle window so that frame flushes.
|
|
28
|
+
const requestExit = useCallback(() => {
|
|
29
|
+
if (exitRequestedRef.current) return;
|
|
30
|
+
exitRequestedRef.current = true;
|
|
31
|
+
setExiting(true);
|
|
32
|
+
const hardExitTimer = setTimeout(() => {
|
|
33
|
+
try { process.stdout.write('\x1b[?25h\x1b[0m'); } catch {}
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}, 2000);
|
|
36
|
+
hardExitTimer.unref?.();
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
let timer = null;
|
|
39
|
+
Promise.race([
|
|
40
|
+
Promise.resolve(store.dispose?.('cli-react-exit', { detach: true })),
|
|
41
|
+
new Promise((resolve) => {
|
|
42
|
+
timer = setTimeout(resolve, 350);
|
|
43
|
+
}),
|
|
44
|
+
]).finally(() => {
|
|
45
|
+
if (timer) clearTimeout(timer);
|
|
46
|
+
exit();
|
|
47
|
+
});
|
|
48
|
+
}, 60);
|
|
49
|
+
}, [store, exit]);
|
|
50
|
+
|
|
51
|
+
const restoreQueuedToPrompt = useCallback((options = {}) => {
|
|
52
|
+
const restoreDraft = options.restoreDraft !== false;
|
|
53
|
+
const showHint = options.showHint !== false;
|
|
54
|
+
const currentText = options.currentText ?? promptValueRef.current ?? promptDraft;
|
|
55
|
+
const restored = store.restoreQueued?.(currentText);
|
|
56
|
+
if (!restored || restored.count === 0) {
|
|
57
|
+
if (showHint) showPromptHint('No queued messages to restore.', 'info');
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (restoreDraft) {
|
|
61
|
+
if (restored.pastedImages) installPastedImages(restored.pastedImages, { merge: true });
|
|
62
|
+
if (restored.pastedTexts) installPastedTexts(restored.pastedTexts, { merge: true });
|
|
63
|
+
syncPromptLayoutRows(restored.text);
|
|
64
|
+
setPromptDraftOverride({ id: Date.now(), value: restored.text });
|
|
65
|
+
}
|
|
66
|
+
if (showHint) {
|
|
67
|
+
showPromptHint(`restored ${restored.count} queued message${restored.count === 1 ? '' : 's'}`, 'info');
|
|
68
|
+
} else {
|
|
69
|
+
clearPromptHint();
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}, [store, promptDraft, showPromptHint, clearPromptHint, installPastedImages, syncPromptLayoutRows]);
|
|
73
|
+
|
|
74
|
+
const recentPromptHistory = useMemo(() => {
|
|
75
|
+
// The engine maintains this list incrementally (rebuilt only when a user
|
|
76
|
+
// item is appended or the transcript is bulk-swapped), so App no longer
|
|
77
|
+
// rescans all items on every transcript change. Fall back to a local scan
|
|
78
|
+
// only if the engine did not publish it (older snapshot).
|
|
79
|
+
if (Array.isArray(state.promptHistoryList)) return state.promptHistoryList;
|
|
80
|
+
const items = Array.isArray(state.items) ? state.items : [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const history = [];
|
|
83
|
+
for (let i = items.length - 1; i >= 0 && history.length < PROMPT_HISTORY_LIMIT; i -= 1) {
|
|
84
|
+
const item = items[i];
|
|
85
|
+
if (item?.kind !== 'user') continue;
|
|
86
|
+
const text = String(item.text || '').trim();
|
|
87
|
+
const key = promptHistoryKey(text);
|
|
88
|
+
if (!key || seen.has(key)) continue;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
history.push(text);
|
|
91
|
+
}
|
|
92
|
+
return history;
|
|
93
|
+
}, [state.promptHistoryList, state.items]);
|
|
94
|
+
|
|
95
|
+
const resetPromptHistoryNav = useCallback(() => {
|
|
96
|
+
promptHistoryNavRef.current = { active: false, index: -1, seed: '', lastValue: '' };
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
return { requestExit, restoreQueuedToPrompt, recentPromptHistory, resetPromptHistoryNav };
|
|
100
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Terminal chrome, extracted from App.jsx: the debounced resize listener
|
|
2
|
+
// (leading-edge + trailing settle, no-op on unchanged dimensions) and the
|
|
3
|
+
// one-shot extended-keyboard enable (kitty + modifyOtherKeys).
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { terminalSize } from './app-format.mjs';
|
|
6
|
+
import { supportsExtendedKeys, ENABLE_KITTY_KEYBOARD, ENABLE_MODIFY_OTHER_KEYS } from '../keyboard-protocol.mjs';
|
|
7
|
+
|
|
8
|
+
export function useTerminalChrome({ stdout, isRawModeSupported, setResizeState }) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdout) return undefined;
|
|
11
|
+
let trailing = null;
|
|
12
|
+
// Leading-edge fire on the first event of a burst, then coalesce the rest:
|
|
13
|
+
// events arriving within DEBOUNCE_MS skip the immediate full-layout update
|
|
14
|
+
// and only refresh the (reset) trailing timer, so a resize storm commits at
|
|
15
|
+
// most once per window plus one settle update instead of once per event.
|
|
16
|
+
const DEBOUNCE_MS = 80;
|
|
17
|
+
let lastRun = 0;
|
|
18
|
+
const update = () => {
|
|
19
|
+
setResizeState((prev) => {
|
|
20
|
+
const next = terminalSize(stdout);
|
|
21
|
+
// No-op when dimensions are unchanged: the unconditional post-mount
|
|
22
|
+
// update() otherwise forces an extra full-frame commit (epoch bump)
|
|
23
|
+
// right after the first paint, which reads as a boot flicker.
|
|
24
|
+
if (next.columns === prev.columns && next.rows === prev.rows) return prev;
|
|
25
|
+
return {
|
|
26
|
+
...next,
|
|
27
|
+
epoch: prev.epoch + 1,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const onResize = () => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - lastRun >= DEBOUNCE_MS) {
|
|
34
|
+
lastRun = now;
|
|
35
|
+
update();
|
|
36
|
+
}
|
|
37
|
+
if (trailing) clearTimeout(trailing);
|
|
38
|
+
trailing = setTimeout(() => {
|
|
39
|
+
trailing = null;
|
|
40
|
+
lastRun = Date.now();
|
|
41
|
+
update();
|
|
42
|
+
}, DEBOUNCE_MS);
|
|
43
|
+
};
|
|
44
|
+
stdout.on('resize', onResize);
|
|
45
|
+
update();
|
|
46
|
+
return () => {
|
|
47
|
+
if (trailing) clearTimeout(trailing);
|
|
48
|
+
stdout.off('resize', onResize);
|
|
49
|
+
};
|
|
50
|
+
}, [stdout]);
|
|
51
|
+
|
|
52
|
+
// Enable extended keyboard reporting (kitty + xterm modifyOtherKeys)
|
|
53
|
+
// SYNCHRONOUSLY, ONCE, with NO query/round-trip. ink
|
|
54
|
+
// turns raw mode on during the first useInput mount (synchronously, inside
|
|
55
|
+
// render); this mount effect runs in the same commit phase, right after — i.e.
|
|
56
|
+
// before the user can realistically press a key. We write BOTH enables
|
|
57
|
+
// unconditionally (the terminal honors whichever it implements; Windows
|
|
58
|
+
// Terminal 1.24 has no kitty but DOES honor modifyOtherKeys), gated only by the
|
|
59
|
+
// supportsExtendedKeys() allowlist. Because the enable lands before the first
|
|
60
|
+
// keypress is read, the FIRST Ctrl+Enter already arrives as a distinguishable
|
|
61
|
+
// \x1b[27;5;13~ (or kitty \x1b[13;5u) instead of a bare \r — fixing the old
|
|
62
|
+
// "first Ctrl+Enter submits, second works" race. Teardown lives in index.jsx's
|
|
63
|
+
// restoreTerminal(). The empty dep array makes this run exactly once on mount.
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!isRawModeSupported || !stdout?.write) return;
|
|
67
|
+
if (!supportsExtendedKeys()) return;
|
|
68
|
+
try {
|
|
69
|
+
stdout.write(ENABLE_KITTY_KEYBOARD + ENABLE_MODIFY_OTHER_KEYS);
|
|
70
|
+
} catch { /* terminal may be closing */ }
|
|
71
|
+
}, []);
|
|
72
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Transcript-activity signatures, extracted from App.jsx: the agent-state
|
|
2
|
+
// revision key, the L2 statusline explore/search pending-tool signature
|
|
3
|
+
// (engine-published fast path + local transcript fallback), and the
|
|
4
|
+
// primitive statusline stats snapshot.
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
// eslint-disable-next-line import/no-relative-packages
|
|
7
|
+
import { classifyToolCategory } from '../../runtime/shared/tool-surface.mjs';
|
|
8
|
+
|
|
9
|
+
export function useTranscriptActivity({ state }) {
|
|
10
|
+
// agentRevision is a cheap change-detection key for downstream consumers, but
|
|
11
|
+
// JSON.stringify over the worker/job arrays ran on EVERY render (including the
|
|
12
|
+
// ~120fps streaming reconciles). Memoize on the agent slices so it only
|
|
13
|
+
// recomputes when agent state actually changes, not on every assistant delta.
|
|
14
|
+
const agentRevision = useMemo(() => JSON.stringify({
|
|
15
|
+
workers: (state.agentWorkers || []).map((w) => [w.tag, w.status, w.stage, w.sessionId]).slice(0, 20),
|
|
16
|
+
jobs: (state.agentJobs || []).map((j) => [j.task_id, j.status, j.tag, j.sessionId, j.startedAt, j.finishedAt, j.error]).slice(0, 20),
|
|
17
|
+
}), [state.agentWorkers, state.agentJobs]);
|
|
18
|
+
|
|
19
|
+
// L2 statusline explore/search segments are the MAIN session's own running
|
|
20
|
+
// tool cards (NOT agentWorkers/agentJobs). Derive pending counts + oldest
|
|
21
|
+
// start time straight from the live transcript tool cards. A card is pending
|
|
22
|
+
// until completedCount >= count. (Do NOT use completedAt as the terminal
|
|
23
|
+
// signal: engine patchToolCardResult stamps completedAt on EVERY aggregate
|
|
24
|
+
// result patch even while calls are still running, and reused tail-aggregates
|
|
25
|
+
// keep a stale completedAt, so it would drop the segment early / skip newly
|
|
26
|
+
// added pending calls.) Aggregate cards carry a `categories` map; standalone
|
|
27
|
+
// cards carry name/args resolved
|
|
28
|
+
// via classifyToolCategory. Keep this CHEAP: build a primitive signature from
|
|
29
|
+
// only the tool items so streaming flushes that swap
|
|
30
|
+
// state.items for a fresh array don't restringify the whole transcript and
|
|
31
|
+
// the StatusLine effect only re-fires when the numbers actually change.
|
|
32
|
+
const activeToolsSignature = useMemo(() => {
|
|
33
|
+
// The engine maintains this signature incrementally (updated on tool
|
|
34
|
+
// start/early-complete/result/turn-end), so App no longer scans every
|
|
35
|
+
// transcript item on each change. Prefer it; fall back to the local scan
|
|
36
|
+
// only when the engine did not publish it (older snapshot).
|
|
37
|
+
if (state.activeToolSummary !== undefined) return state.activeToolSummary || '';
|
|
38
|
+
const items = state.items || [];
|
|
39
|
+
let exploreCount = 0;
|
|
40
|
+
let exploreStart = 0;
|
|
41
|
+
let searchCount = 0;
|
|
42
|
+
let searchStart = 0;
|
|
43
|
+
for (const it of items) {
|
|
44
|
+
if (!it || it.kind !== 'tool') continue;
|
|
45
|
+
const count = Math.max(1, Number(it.count || 1));
|
|
46
|
+
// Resolved check: aggregates stay on the pure completedCount>=count test
|
|
47
|
+
// because engine patchToolCardResult sets `result` on EVERY aggregate
|
|
48
|
+
// patch (even partial, completedCount<count), so a `result`-aware check
|
|
49
|
+
// would drop a still-running aggregate early. Standalone cards mirror
|
|
50
|
+
// toolItemPendingForRows (done when completedCount>=count OR a result
|
|
51
|
+
// landed) so an abnormally-finished card (cancelled/errored) that sets a
|
|
52
|
+
// result without bumping completedCount cannot pin a phantom segment.
|
|
53
|
+
const done = it.aggregate
|
|
54
|
+
? Number(it.completedCount || 0)
|
|
55
|
+
: Math.max(0, Math.min(count, Number(it.completedCount || (it.result == null ? 0 : count))));
|
|
56
|
+
if (done >= count) continue; // resolved card (matches toolItemPendingForRows)
|
|
57
|
+
const started = Number(it.startedAt || 0);
|
|
58
|
+
let exploreHits = 0;
|
|
59
|
+
let searchHits = 0;
|
|
60
|
+
if (it.aggregate && it.categories && typeof it.categories === 'object') {
|
|
61
|
+
for (const v of Object.values(it.categories)) {
|
|
62
|
+
const cat = v && typeof v === 'object' ? v.category : null;
|
|
63
|
+
const c = Math.max(1, Number(v && typeof v === 'object' ? v.count : 1) || 1);
|
|
64
|
+
if (cat === 'Explore') exploreHits += c;
|
|
65
|
+
else if (cat === 'Web Research') searchHits += c;
|
|
66
|
+
}
|
|
67
|
+
} else if (it.name) {
|
|
68
|
+
const cat = classifyToolCategory(it.name, it.args || {});
|
|
69
|
+
if (cat === 'Explore') exploreHits = count;
|
|
70
|
+
else if (cat === 'Web Research') searchHits = count;
|
|
71
|
+
}
|
|
72
|
+
if (exploreHits > 0) {
|
|
73
|
+
exploreCount += exploreHits;
|
|
74
|
+
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
75
|
+
}
|
|
76
|
+
if (searchHits > 0) {
|
|
77
|
+
searchCount += searchHits;
|
|
78
|
+
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!exploreCount && !searchCount) return '';
|
|
82
|
+
return `${exploreCount}:${exploreStart}:${searchCount}:${searchStart}`;
|
|
83
|
+
}, [state.activeToolSummary, state.items]);
|
|
84
|
+
|
|
85
|
+
const activeTools = useMemo(() => {
|
|
86
|
+
if (!activeToolsSignature) return null;
|
|
87
|
+
const [ec, es, sc, ss] = activeToolsSignature.split(':').map((n) => Number(n) || 0);
|
|
88
|
+
return {
|
|
89
|
+
explore: { count: ec, startedAt: es },
|
|
90
|
+
search: { count: sc, startedAt: ss },
|
|
91
|
+
};
|
|
92
|
+
}, [activeToolsSignature]);
|
|
93
|
+
|
|
94
|
+
// StatusLine only reads a small stats subset; engine clones the full stats
|
|
95
|
+
// object on many updates. Memoize by field value so identical usage keeps
|
|
96
|
+
// the same object reference and React.memo / effect deps stay quiet.
|
|
97
|
+
const statuslineStats = useMemo(() => {
|
|
98
|
+
const s = state.stats || {};
|
|
99
|
+
return {
|
|
100
|
+
currentContextSource: s.currentContextSource ?? null,
|
|
101
|
+
currentEstimatedContextTokens: s.currentEstimatedContextTokens ?? 0,
|
|
102
|
+
currentContextTokens: s.currentContextTokens ?? 0,
|
|
103
|
+
contextTokens: s.contextTokens ?? 0,
|
|
104
|
+
latestPromptTokens: s.latestPromptTokens ?? 0,
|
|
105
|
+
latestInputTokens: s.latestInputTokens ?? 0,
|
|
106
|
+
latestCachedTokens: s.latestCachedTokens ?? 0,
|
|
107
|
+
latestCacheWriteTokens: s.latestCacheWriteTokens ?? 0,
|
|
108
|
+
inputTokens: s.inputTokens ?? 0,
|
|
109
|
+
cachedTokens: s.cachedTokens ?? 0,
|
|
110
|
+
cacheWriteTokens: s.cacheWriteTokens ?? 0,
|
|
111
|
+
promptTokens: s.promptTokens ?? 0,
|
|
112
|
+
turns: s.turns ?? 0,
|
|
113
|
+
};
|
|
114
|
+
}, [
|
|
115
|
+
state.stats?.currentContextSource,
|
|
116
|
+
state.stats?.currentEstimatedContextTokens,
|
|
117
|
+
state.stats?.currentContextTokens,
|
|
118
|
+
state.stats?.contextTokens,
|
|
119
|
+
state.stats?.latestPromptTokens,
|
|
120
|
+
state.stats?.latestInputTokens,
|
|
121
|
+
state.stats?.latestCachedTokens,
|
|
122
|
+
state.stats?.latestCacheWriteTokens,
|
|
123
|
+
state.stats?.inputTokens,
|
|
124
|
+
state.stats?.cachedTokens,
|
|
125
|
+
state.stats?.cacheWriteTokens,
|
|
126
|
+
state.stats?.promptTokens,
|
|
127
|
+
state.stats?.turns,
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
return { agentRevision, activeToolsSignature, activeTools, statuslineStats };
|
|
131
|
+
}
|