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.
Files changed (180) hide show
  1. package/package.json +14 -8
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +5 -1
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +59 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +147 -16
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +144 -10
  43. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  44. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  45. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  46. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  48. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  49. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  50. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  52. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  53. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  54. package/src/runtime/agent/orchestrator/session/manager.mjs +8 -1
  55. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  56. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  57. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  58. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  59. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  60. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  61. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  62. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  63. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  64. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  65. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  66. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  67. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  69. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  70. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  71. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  72. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  73. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  75. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  76. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  77. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  79. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  81. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  82. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  83. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  84. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  85. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +67 -3
  86. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  87. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +203 -38
  88. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -1
  89. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +73 -19
  90. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  91. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -8
  92. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  93. package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
  94. package/src/runtime/media/lanes.mjs +2 -2
  95. package/src/runtime/media/renditions.mjs +101 -17
  96. package/src/runtime/media/renditions.test.mjs +54 -0
  97. package/src/runtime/media/store.mjs +111 -43
  98. package/src/runtime/media/store.test.mjs +53 -11
  99. package/src/runtime/memory/index.mjs +26 -50
  100. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  101. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  102. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  103. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  104. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  105. package/src/runtime/memory/lib/http-router.mjs +4 -0
  106. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +25 -19
  108. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  109. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  111. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  112. package/src/runtime/memory/tool-defs.mjs +22 -6
  113. package/src/runtime/shared/atomic-file.mjs +5 -1
  114. package/src/runtime/shared/child-guardian.mjs +52 -2
  115. package/src/runtime/shared/provider-api-key.mjs +22 -5
  116. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  117. package/src/session-runtime/config-helpers.mjs +6 -5
  118. package/src/session-runtime/lifecycle-api.mjs +27 -0
  119. package/src/session-runtime/media-api.mjs +31 -16
  120. package/src/session-runtime/prewarm.mjs +2 -24
  121. package/src/session-runtime/provider-auth-api.mjs +8 -0
  122. package/src/session-runtime/provider-usage.mjs +30 -5
  123. package/src/session-runtime/runtime-core.mjs +298 -494
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +372 -0
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +383 -0
  131. package/src/standalone/agent-tool/spawn-flow.mjs +438 -0
  132. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  133. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  134. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  135. package/src/standalone/agent-tool.mjs +115 -1166
  136. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  137. package/src/standalone/channel-daemon.mjs +8 -0
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +47 -3
  141. package/src/tui/App.jsx +367 -2579
  142. package/src/tui/app/app-view.jsx +504 -0
  143. package/src/tui/app/create-app-pickers.mjs +313 -0
  144. package/src/tui/app/prompt-submit.mjs +501 -0
  145. package/src/tui/app/resume-picker.mjs +7 -2
  146. package/src/tui/app/shell-layout.mjs +563 -0
  147. package/src/tui/app/slash-dispatch.mjs +13 -3
  148. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  149. package/src/tui/app/transcript-window.mjs +17 -93
  150. package/src/tui/app/usage-context-panels.mjs +288 -0
  151. package/src/tui/app/use-copy-selection.mjs +56 -0
  152. package/src/tui/app/use-global-key-input.mjs +151 -0
  153. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  154. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  155. package/src/tui/app/use-prompt-hint.mjs +58 -0
  156. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  157. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  158. package/src/tui/app/use-transcript-activity.mjs +131 -0
  159. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  160. package/src/tui/app/use-transcript-window.mjs +64 -114
  161. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  162. package/src/tui/components/Markdown.jsx +19 -2
  163. package/src/tui/components/Message.jsx +8 -3
  164. package/src/tui/components/ToolExecution.jsx +7 -3
  165. package/src/tui/dist/index.mjs +9889 -8999
  166. package/src/tui/engine/live-share.mjs +15 -5
  167. package/src/tui/engine/render-timing.mjs +2 -2
  168. package/src/tui/engine/session-api-ext.mjs +97 -4
  169. package/src/tui/engine/session-flow.mjs +3 -1
  170. package/src/tui/engine.mjs +2 -2
  171. package/src/tui/hooks/useSharedTick.mjs +0 -2
  172. package/src/tui/index.jsx +5 -5
  173. package/src/ui/statusline-agents.mjs +44 -11
  174. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  175. package/src/workflows/default/WORKFLOW.md +3 -3
  176. package/src/workflows/solo/WORKFLOW.md +1 -1
  177. package/src/runtime/media/index.mjs +0 -18
  178. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  179. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  180. 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
+ }