mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,47 @@
1
+ /**
2
+ * components/ConfirmBar.jsx — horizontal text-button bar for onboarding steps.
3
+ *
4
+ * Pure render + helpers. Owns NO keyboard state: the parent Picker manages
5
+ * `focusedIndex` (0..n-1) and dispatches Enter to `onConfirm`. Kept side-effect
6
+ * free so it can be reused under any picker without stealing input focus.
7
+ *
8
+ * Props:
9
+ * buttons: [{ value, label }] — button descriptors
10
+ * focusedIndex: number — highlighted button (-1 = none/list focus)
11
+ */
12
+ import React from 'react';
13
+ import { Box, Text } from 'ink';
14
+ import { theme } from '../theme.mjs';
15
+
16
+ /** Clamp a focus index into the valid button range, or -1 when list-focused. */
17
+ export function clampConfirmFocus(index, count) {
18
+ const n = Math.max(0, Number(count) || 0);
19
+ if (n === 0) return -1;
20
+ const i = Number(index);
21
+ if (!Number.isFinite(i) || i < 0) return -1;
22
+ return Math.min(i, n - 1);
23
+ }
24
+
25
+ export function ConfirmBar({ buttons = [], focusedIndex = -1 }) {
26
+ const list = Array.isArray(buttons) ? buttons.filter(Boolean) : [];
27
+ if (list.length === 0) return null;
28
+ return (
29
+ <Box flexDirection="row" width="100%" justifyContent="flex-end">
30
+ {list.map((button, index) => {
31
+ const isFocused = index === focusedIndex;
32
+ return (
33
+ <Text key={button.value ?? index}>
34
+ {index > 0 ? <Text color={theme.subtle}>{' '}</Text> : null}
35
+ <Text
36
+ color={isFocused ? theme.selectionText : theme.text}
37
+ backgroundColor={isFocused ? theme.selectionBackground : undefined}
38
+ bold={isFocused}
39
+ >
40
+ {` ${button.label} `}
41
+ </Text>
42
+ </Text>
43
+ );
44
+ })}
45
+ </Box>
46
+ );
47
+ }
@@ -47,7 +47,7 @@ function percent(value, total) {
47
47
 
48
48
  function percentLabel(value, total) {
49
49
  const pct = percent(value, total);
50
- if (pct === null) return 'n/a';
50
+ if (pct === null) return 'N/A';
51
51
  return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.round(pct)}%`;
52
52
  }
53
53
 
@@ -185,8 +185,10 @@ function ContextUsageView({ detail, columns }) {
185
185
  ]);
186
186
  const apiLine = metricValue([
187
187
  `last ctx ${formatTokens(lastApi.contextTokens)}`,
188
- `in/out ${formatTokens(lastApi.inputTokens)}/${formatTokens(lastApi.outputTokens)}`,
189
- `cache ${cache.hitRate || 'n/a'}`,
188
+ `uncached/out ${formatTokens(lastApi.inputTokens)}/${formatTokens(lastApi.outputTokens)}`,
189
+ lastApi.rawInputTokens && lastApi.rawInputTokens !== lastApi.inputTokens ? `raw in ${formatTokens(lastApi.rawInputTokens)}` : '',
190
+ cache.writeTokens ? `write ${formatTokens(cache.writeTokens)}` : '',
191
+ `cache ${cache.hitRate || 'N/A'}`,
190
192
  ]);
191
193
  const categories = [
192
194
  { label: 'Messages', tokens: semanticTokens(semantic, ['chat', 'assistant']), meta: '' },
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Overprint a truncated right-side hint on the last row of a transcript item
3
+ * without reserving an extra layout row (negative margin pulls the hint band up).
4
+ */
5
+ import React from 'react';
6
+ import { Box, Text } from 'ink';
7
+ import { theme } from '../theme.mjs';
8
+
9
+ function hintColor(tone) {
10
+ if (tone === 'error') return theme.error;
11
+ if (tone === 'warn' || tone === 'cancel') return theme.warning;
12
+ if (tone === 'plain') return theme.subtle;
13
+ return theme.inactive;
14
+ }
15
+
16
+ function cleanRightMessage(value) {
17
+ return String(value || '').replace(/\s+/g, ' ').trim();
18
+ }
19
+
20
+ export function ItemRightHintOverprint({
21
+ children,
22
+ rightMessage = '',
23
+ rightTone = 'info',
24
+ rightMessageWidth = 24,
25
+ }) {
26
+ const rightText = cleanRightMessage(rightMessage);
27
+ if (!rightText) return children;
28
+ const rightWidth = Math.max(1, Number(rightMessageWidth) || 24);
29
+ return (
30
+ <Box flexDirection="column" width="100%" flexShrink={0}>
31
+ {children}
32
+ <Box
33
+ height={1}
34
+ marginTop={-1}
35
+ flexDirection="row"
36
+ width="100%"
37
+ flexShrink={0}
38
+ overflow="hidden"
39
+ >
40
+ <Box flexGrow={1} flexShrink={1} overflow="hidden" />
41
+ <Box
42
+ flexShrink={0}
43
+ width={rightWidth}
44
+ marginLeft={1}
45
+ marginRight={1}
46
+ justifyContent="flex-end"
47
+ overflow="hidden"
48
+ >
49
+ <Text color={hintColor(rightTone)} wrap="truncate">{rightText}</Text>
50
+ </Box>
51
+ </Box>
52
+ </Box>
53
+ );
54
+ }
@@ -12,19 +12,28 @@
12
12
  * Syntax highlighting is omitted, but token cache + streaming-split are
13
13
  * kept so partial markdown does not repaint stable text on every delta.
14
14
  */
15
- import React, { useRef } from 'react';
15
+ import React from 'react';
16
16
  import { Box, Text } from 'ink';
17
- import { marked } from 'marked';
18
17
  import {
19
- configureMarked,
20
- hasMarkdownSyntax,
21
18
  renderTokenAnsiSegments,
22
19
  } from '../markdown/render-ansi.mjs';
23
- import { trimPartialClosingFences } from '../markdown/stream-fence.mjs';
20
+ import { resolveStreamingMarkdownParts } from '../markdown/streaming-markdown.mjs';
24
21
  import { AnsiText } from './AnsiText.jsx';
25
22
  import { MarkdownTable } from './MarkdownTable.jsx';
26
23
  import { theme } from '../theme.mjs';
27
24
 
25
+ export {
26
+ balanceStreamingMarkdown,
27
+ resolveStreamingMarkdownParts,
28
+ resetStreamingMarkdownStablePrefix,
29
+ resetAllStreamingMarkdownStablePrefixes,
30
+ streamingLayoutText,
31
+ } from '../markdown/streaming-markdown.mjs';
32
+ export {
33
+ measureMarkdownRenderedRows,
34
+ measureStreamingMarkdownRenderedRows,
35
+ } from '../markdown/measure-rendered-rows.mjs';
36
+
28
37
  function renderMarkdownElements(content, trimPartialFences = false, tableWidth) {
29
38
  // `tableWidth` is the App's body/content width; it doubles as the hr fill
30
39
  // width. Fall back to 80 when not provided so an hr still spans a sane rule.
@@ -50,60 +59,6 @@ function renderMarkdownElements(content, trimPartialFences = false, tableWidth)
50
59
  return result;
51
60
  }
52
61
 
53
- function hasOpenFence(text) {
54
- let ticks = 0;
55
- let tildes = 0;
56
- for (const line of String(text ?? '').split('\n')) {
57
- if (/^\s*```/.test(line)) ticks += 1;
58
- if (/^\s*~~~/.test(line)) tildes += 1;
59
- }
60
- return ticks % 2 === 1 || tildes % 2 === 1;
61
- }
62
-
63
- function hasOpenInlineCode(text) {
64
- let count = 0;
65
- const value = String(text ?? '');
66
- for (let i = 0; i < value.length; i++) {
67
- const ch = value[i];
68
- if (ch === '\\') {
69
- i += 1;
70
- continue;
71
- }
72
- if (ch !== '`') continue;
73
- let run = 1;
74
- while (value[i + run] === '`') run += 1;
75
- if (run === 1) count += 1;
76
- i += run - 1;
77
- }
78
- return count % 2 === 1;
79
- }
80
-
81
- function hasUnclosedDelimiter(text, marker) {
82
- let count = 0;
83
- const value = String(text ?? '');
84
- for (let i = 0; i < value.length; i++) {
85
- if (value[i] === '\\') {
86
- i += 1;
87
- continue;
88
- }
89
- if (value.startsWith(marker, i)) {
90
- count += 1;
91
- i += marker.length - 1;
92
- }
93
- }
94
- return count % 2 === 1;
95
- }
96
-
97
- function balanceStreamingMarkdown(text) {
98
- const value = String(text ?? '');
99
- if (!value || hasOpenFence(value)) return value;
100
- if (hasOpenInlineCode(value)) return `${value}\``;
101
- let rendered = value;
102
- if (hasUnclosedDelimiter(rendered, '**')) rendered += '**';
103
- if (hasUnclosedDelimiter(rendered, '__')) rendered += '__';
104
- return rendered;
105
- }
106
-
107
62
  export function Markdown({ children, themeEpoch = 0, trimPartialFences = false, columns }) {
108
63
  const elements = React.useMemo(() => {
109
64
  try {
@@ -124,48 +79,17 @@ export function Markdown({ children, themeEpoch = 0, trimPartialFences = false,
124
79
  );
125
80
  }
126
81
 
127
- export function StreamingMarkdown({ children, themeEpoch = 0, columns }) {
128
- const stablePrefixRef = useRef('');
129
- const text = String(children ?? '');
130
-
131
- if (!hasMarkdownSyntax(text)) {
132
- stablePrefixRef.current = '';
133
- return <Markdown themeEpoch={themeEpoch} columns={columns}>{text}</Markdown>;
134
- }
135
-
136
- if (!text.startsWith(stablePrefixRef.current)) {
137
- stablePrefixRef.current = '';
82
+ export function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
83
+ const parts = resolveStreamingMarkdownParts(children, streamKey);
84
+ if (parts.plain) {
85
+ return <Markdown themeEpoch={themeEpoch} columns={columns}>{parts.unstableForRender}</Markdown>;
138
86
  }
139
-
140
- let stablePrefix = stablePrefixRef.current;
141
- try {
142
- configureMarked();
143
- const boundary = stablePrefix.length;
144
- // Lex the still-streaming suffix and trim any partial closing fence so an
145
- // open code block does not grow-then-shrink as the final backtick(s)
146
- // stream in. Operates on a fresh lex so the shared tokenCache is untouched.
147
- const tokens = marked.lexer(text.substring(boundary));
148
- trimPartialClosingFences(tokens);
149
- let lastContentIdx = tokens.length - 1;
150
- while (lastContentIdx >= 0 && tokens[lastContentIdx]?.type === 'space') lastContentIdx--;
151
- let advance = 0;
152
- for (let i = 0; i < lastContentIdx; i++) {
153
- advance += tokens[i]?.raw?.length ?? 0;
154
- }
155
- if (advance > 0) {
156
- stablePrefixRef.current = text.substring(0, boundary + advance);
157
- stablePrefix = stablePrefixRef.current;
158
- }
159
- } catch {
160
- stablePrefix = '';
161
- stablePrefixRef.current = '';
162
- }
163
-
164
- const unstableSuffix = text.substring(stablePrefix.length);
165
87
  return (
166
88
  <Box flexDirection="column" gap={1}>
167
- {stablePrefix ? <Markdown themeEpoch={themeEpoch} columns={columns}>{stablePrefix}</Markdown> : null}
168
- {unstableSuffix ? <Markdown themeEpoch={themeEpoch} columns={columns} trimPartialFences>{balanceStreamingMarkdown(unstableSuffix)}</Markdown> : null}
89
+ {parts.stablePrefix ? <Markdown themeEpoch={themeEpoch} columns={columns}>{parts.stablePrefix}</Markdown> : null}
90
+ {parts.unstableSuffix
91
+ ? <Markdown themeEpoch={themeEpoch} columns={columns} trimPartialFences>{parts.unstableForRender}</Markdown>
92
+ : null}
169
93
  </Box>
170
94
  );
171
95
  }
@@ -13,18 +13,22 @@
13
13
  * not under the dot (2-wide gutter alignment).
14
14
  */
15
15
  import React from 'react';
16
- import { Box, Text, useAnimation } from 'ink';
16
+ import { Box, Text } from 'ink';
17
17
  import { theme, TURN_MARKER } from '../theme.mjs';
18
- import { Markdown, StreamingMarkdown } from './Markdown.jsx';
18
+ import { Markdown, StreamingMarkdown, resetStreamingMarkdownStablePrefix } from './Markdown.jsx';
19
19
  import { assistantBodyWidth } from '../markdown/table-layout.mjs';
20
- import { THEREFORE } from '../figures.mjs';
21
- import { formatDuration } from '../time-format.mjs';
22
20
 
23
21
  // `themeEpoch` is a memo-busting prop (threaded from App): the active theme
24
22
  // mutates `theme` in-place, so a /theme switch must re-render this memoized row
25
23
  // and recompute its markdown colors. It is forwarded into Markdown so the
26
24
  // token/AnsiText caches include it as a dep.
27
- export const AssistantMessage = React.memo(function AssistantMessage({ text, streaming = false, columns = 80, themeEpoch = 0 }) {
25
+ export const AssistantMessage = React.memo(function AssistantMessage({
26
+ text,
27
+ streaming = false,
28
+ columns = 80,
29
+ themeEpoch = 0,
30
+ assistantId,
31
+ }) {
28
32
  // The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
29
33
  // measures the wrapped markdown body before the row's available width is
30
34
  // resolved and caches its height as a single row — so a multi-line assistant
@@ -34,6 +38,10 @@ export const AssistantMessage = React.memo(function AssistantMessage({ text, str
34
38
  // assistant line reach the terminal's last column can auto-wrap/scroll on
35
39
  // Windows Terminal/conhost and make the next redraw appear to lose leading
36
40
  // CJK characters even though the backing transcript is intact.
41
+ React.useEffect(() => {
42
+ if (!streaming && assistantId) resetStreamingMarkdownStablePrefix(assistantId);
43
+ }, [streaming, assistantId]);
44
+
37
45
  const bodyWidth = assistantBodyWidth(columns);
38
46
  return (
39
47
  <Box flexDirection="row" marginTop={1}>
@@ -42,7 +50,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({ text, str
42
50
  </Box>
43
51
  <Box flexDirection="column" flexShrink={0} width={bodyWidth}>
44
52
  {streaming
45
- ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</StreamingMarkdown>
53
+ ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{text}</StreamingMarkdown>
46
54
  : <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
47
55
  </Box>
48
56
  </Box>
@@ -68,35 +76,6 @@ export const UserMessage = React.memo(function UserMessage({ text, attached = fa
68
76
  );
69
77
  });
70
78
 
71
- function formatThinkingElapsed(ms) {
72
- const label = formatDuration(ms);
73
- if (!label) return '';
74
- const totalSec = Math.floor(Math.max(0, Number(ms || 0)) / 1000);
75
- if (totalSec < 60) return label;
76
- const m = Math.floor(totalSec / 60);
77
- const s = totalSec % 60;
78
- return `${m}:${String(s).padStart(2, '0')}`;
79
- }
80
-
81
- export function ThinkingMessage({ text, elapsedMs = 0, activeSince = 0 }) {
82
- useAnimation({ interval: 250 });
83
- const liveElapsedMs = Number(elapsedMs || 0) + (activeSince ? Math.max(0, Date.now() - activeSince) : 0);
84
- const elapsed = formatThinkingElapsed(liveElapsedMs);
85
- return (
86
- <Box flexDirection="column" marginTop={1} gap={1} width="100%">
87
- <Text>
88
- <Text color={theme.spinnerGlyph}>{THEREFORE} </Text>
89
- <Text color={theme.thinkingAccent}>{`Thinking${elapsed ? ` ${elapsed}` : ''}…`}</Text>
90
- </Text>
91
- {text ? (
92
- <Box paddingLeft={2}>
93
- <Text color={theme.thinkingText} italic>{text}</Text>
94
- </Box>
95
- ) : null}
96
- </Box>
97
- );
98
- }
99
-
100
79
  export function NoticeMessage({ text, tone, columns = 80 }) {
101
80
  const accentColor = tone === 'error' ? theme.error : tone === 'warn' ? theme.warning : theme.inactive;
102
81
  const bodyColor = tone === 'info' || tone === 'plain' ? theme.inactive : theme.statusText;
@@ -17,12 +17,14 @@ import React, { useState, useCallback, useEffect } from 'react';
17
17
  import { Box, Text, useInput } from 'ink';
18
18
  import stringWidth from 'string-width';
19
19
  import { theme } from '../theme.mjs';
20
+ import { ConfirmBar, clampConfirmFocus } from './ConfirmBar.jsx';
20
21
 
21
22
  /** Max items visible at once before scrolling kicks in. */
22
23
  const MAX_VISIBLE = 8;
23
24
  const DEFAULT_LABEL_WIDTH = 28;
24
25
  const SELECT_HELP = '↑/↓ Select · Enter Choose · Esc Back';
25
26
  const ADJUST_HELP = '↑/↓ Select · ←/→ Adjust · Enter Choose · Esc Back';
27
+ const CONFIRM_HELP = '↑/↓ Select · ←/→ Back/Next · Enter Choose · Esc Skip';
26
28
 
27
29
  function truncateText(value, width) {
28
30
  const text = String(value || '');
@@ -92,6 +94,10 @@ export function Picker({
92
94
  indexMode = 'auto',
93
95
  fillHeight = false,
94
96
  visibleCount = MAX_VISIBLE,
97
+ // Onboarding confirm bar: { buttons:[{value,label}], onConfirm(button,index) }.
98
+ // When present, ←/→ and Tab drive button focus (mutually exclusive with
99
+ // onLeft/onRight), and Enter fires onConfirm while a button is focused.
100
+ confirmBar = null,
95
101
  // Memo-busting epoch: ItemRow is React.memo and reads theme.* directly, so a
96
102
  // live /theme switch (or picker preview) must re-render every row. Threading
97
103
  // the epoch into each ItemRow breaks its shallow-equality on a theme change.
@@ -99,6 +105,14 @@ export function Picker({
99
105
  }) {
100
106
  const visibleLimit = Math.max(1, Math.floor(Number(visibleCount) || MAX_VISIBLE));
101
107
  const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, Math.min(Number(initialIndex) || 0, Math.max(0, items.length - 1))));
108
+ const confirmButtons = Array.isArray(confirmBar?.buttons) ? confirmBar.buttons.filter(Boolean) : [];
109
+ const hasConfirm = confirmButtons.length > 0;
110
+ // -1 = list focus; 0..n-1 = confirm-bar button focus.
111
+ const [confirmFocus, setConfirmFocus] = useState(-1);
112
+ useEffect(() => {
113
+ // Reset to list focus whenever the bar identity/shape changes (step switch).
114
+ setConfirmFocus(-1);
115
+ }, [confirmButtons.length, confirmBar]);
102
116
 
103
117
  useEffect(() => {
104
118
  setSelectedIndex((i) => Math.min(Math.max(0, i), Math.max(0, items.length - 1)));
@@ -122,13 +136,24 @@ export function Picker({
122
136
  const footerLines = normalizeFooterLines(activeFooter, columns);
123
137
  const footerGap = footerLines.length > 0 ? Math.max(0, Math.floor(Number(footerGapRows) || 0)) : 0;
124
138
  const footerReserveRows = footerLines.length > 0 ? footerLines.length + footerGap : 0;
125
- const effectiveVisibleLimit = Math.max(1, visibleLimit - footerReserveRows);
126
- const helpText = help || (onLeft || onRight || onTab ? ADJUST_HELP : SELECT_HELP);
139
+ const confirmReserveRows = hasConfirm ? 2 : 0;
140
+ const effectiveVisibleLimit = Math.max(1, visibleLimit - footerReserveRows - confirmReserveRows);
141
+ const helpText = help || (hasConfirm ? CONFIRM_HELP : (onLeft || onRight || onTab ? ADJUST_HELP : SELECT_HELP));
127
142
 
128
143
  useInput(
129
144
  useCallback(
130
145
  (input, key) => {
131
146
  if (key.upArrow) {
147
+ // Single vertical loop over [list items...] + [confirm buttons...].
148
+ if (hasConfirm) {
149
+ const last = items.length - 1;
150
+ if (confirmFocus > 0) { setConfirmFocus((f) => f - 1); return; }
151
+ if (confirmFocus === 0) { setConfirmFocus(-1); setSelectedIndex(Math.max(0, last)); return; }
152
+ // list focus: first row ↑ → last confirm button.
153
+ if (items.length === 0 || selectedIndex === 0) { setConfirmFocus(confirmButtons.length - 1); return; }
154
+ setSelectedIndex((i) => i - 1);
155
+ return;
156
+ }
132
157
  setSelectedIndex((i) => {
133
158
  const total = items.length;
134
159
  return total > 0 ? (i - 1 + total) % total : 0;
@@ -136,6 +161,19 @@ export function Picker({
136
161
  return;
137
162
  }
138
163
  if (key.downArrow) {
164
+ if (hasConfirm) {
165
+ const last = items.length - 1;
166
+ const lastBtn = confirmButtons.length - 1;
167
+ if (confirmFocus >= 0) {
168
+ if (confirmFocus < lastBtn) { setConfirmFocus((f) => f + 1); return; }
169
+ // last button ↓ → first list row.
170
+ setConfirmFocus(-1); setSelectedIndex(0); return;
171
+ }
172
+ // list focus: last row ↓ → first confirm button.
173
+ if (items.length === 0 || selectedIndex === last) { setConfirmFocus(0); return; }
174
+ setSelectedIndex((i) => i + 1);
175
+ return;
176
+ }
139
177
  setSelectedIndex((i) => {
140
178
  const total = items.length;
141
179
  return total > 0 ? (i + 1) % total : 0;
@@ -159,18 +197,35 @@ export function Picker({
159
197
  return;
160
198
  }
161
199
  if (key.leftArrow) {
200
+ if (hasConfirm) {
201
+ setConfirmFocus((f) => (f <= 0 ? -1 : f - 1));
202
+ return;
203
+ }
162
204
  if (onLeft) onLeft(items[selectedIndex], selectedIndex);
163
205
  return;
164
206
  }
165
207
  if (key.rightArrow) {
208
+ if (hasConfirm) {
209
+ setConfirmFocus((f) => (f < 0 ? 0 : Math.min(confirmButtons.length - 1, f + 1)));
210
+ return;
211
+ }
166
212
  if (onRight) onRight(items[selectedIndex], selectedIndex);
167
213
  return;
168
214
  }
169
215
  if (key.tab || input === '\t') {
216
+ if (hasConfirm) {
217
+ setConfirmFocus((f) => (f < 0 ? 0 : (f + 1 > confirmButtons.length - 1 ? -1 : f + 1)));
218
+ return;
219
+ }
170
220
  if (onTab) onTab(items[selectedIndex], selectedIndex);
171
221
  return;
172
222
  }
173
223
  if (key.return) {
224
+ if (hasConfirm && confirmFocus >= 0) {
225
+ const button = confirmButtons[confirmFocus];
226
+ if (button && confirmBar?.onConfirm) confirmBar.onConfirm(button, confirmFocus);
227
+ return;
228
+ }
174
229
  const selected = items[selectedIndex];
175
230
  if (selected && onSelect) onSelect(selected.value, selected);
176
231
  return;
@@ -187,7 +242,7 @@ export function Picker({
187
242
  return;
188
243
  }
189
244
  },
190
- [items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, onKey, effectiveVisibleLimit],
245
+ [items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, onKey, effectiveVisibleLimit, hasConfirm, confirmFocus, confirmButtons, confirmBar],
191
246
  ),
192
247
  );
193
248
 
@@ -214,6 +269,13 @@ export function Picker({
214
269
  <Text color={theme.text}>{emptyLine || ' '}</Text>
215
270
  <Text> </Text>
216
271
  <Text color={theme.inactive}>(empty)</Text>
272
+ {hasConfirm ? (
273
+ <>
274
+ <Box flexGrow={1} />
275
+ <Text> </Text>
276
+ <ConfirmBar buttons={confirmButtons} focusedIndex={clampConfirmFocus(confirmFocus, confirmButtons.length)} />
277
+ </>
278
+ ) : null}
217
279
  </Box>
218
280
  </Box>
219
281
  );
@@ -270,7 +332,7 @@ export function Picker({
270
332
  <Text> </Text>
271
333
  {visible.map((item, i) => {
272
334
  const idx = start + i;
273
- const isSelected = idx === selectedIndex;
335
+ const isSelected = idx === selectedIndex && confirmFocus < 0;
274
336
  return (
275
337
  <ItemRow
276
338
  key={item.value}
@@ -305,12 +367,21 @@ export function Picker({
305
367
  ))}
306
368
  </>
307
369
  ) : null}
370
+ {hasConfirm ? (
371
+ <>
372
+ {footerLines.length > 0 ? null : <Box flexGrow={1} />}
373
+ <Text> </Text>
374
+ <ConfirmBar buttons={confirmButtons} focusedIndex={clampConfirmFocus(confirmFocus, confirmButtons.length)} />
375
+ </>
376
+ ) : null}
308
377
  </Box>
309
378
  </Box>
310
379
  );
311
380
  }
312
381
 
313
382
  const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, markerColor, markerWidth, label, labelSuffix, labelSuffixColor, meta, metaParts, description, labelWidth, metaWidth, descriptionWidth, showMeta, isSelected, themeEpoch = 0 }) {
383
+ const rowText = isSelected ? theme.selectionText : theme.text;
384
+ const rowIndexColor = isSelected ? theme.selectionText : theme.subtle;
314
385
  const rawSuffix = String(labelSuffix || '');
315
386
  const suffix = rawSuffix ? truncateText(rawSuffix, labelWidth) : '';
316
387
  const suffixGap = suffix && stringWidth(suffix) < labelWidth ? ' ' : '';
@@ -323,22 +394,26 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, mar
323
394
  const parts = Array.isArray(metaParts) ? metaParts : null;
324
395
 
325
396
  return (
326
- <Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.userMessageBackground : undefined}>
397
+ <Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.selectionBackground : undefined}>
327
398
  {indexWidth > 0 ? (
328
- <Text color={theme.subtle}>
399
+ <Text color={rowIndexColor}>
329
400
  {padCells(indexText, indexWidth)}{' '}
330
401
  </Text>
331
402
  ) : null}
332
403
  {markerWidth > 0 ? (
333
- <Text color={marker ? (markerColor || theme.success) : theme.text}>
404
+ <Text color={isSelected ? theme.selectionText : (marker ? (markerColor || theme.success) : rowText)}>
334
405
  {padCells(displayMarker, markerWidth)}
335
406
  </Text>
336
407
  ) : null}
337
- <Text color={theme.text}>{displayLabel}</Text>
338
- {suffix ? <Text color={labelSuffixColor || theme.success}>{suffixGap}{suffix}</Text> : null}
339
- <Text color={theme.text}>{labelPadding}</Text>
408
+ <Text color={rowText}>{displayLabel}</Text>
409
+ {suffix ? (
410
+ <Text color={isSelected ? theme.selectionText : (labelSuffixColor || theme.success)}>
411
+ {suffixGap}{suffix}
412
+ </Text>
413
+ ) : null}
414
+ <Text color={rowText}>{labelPadding}</Text>
340
415
  {showMeta ? (
341
- <Text color={theme.text}>
416
+ <Text color={rowText}>
342
417
  {' '}
343
418
  {parts
344
419
  ? padCells(parts.map((part) => padCells(truncateText(part?.text || '', Number(part?.width) || 1), Number(part?.width) || 1)).join(' '), metaWidth)
@@ -346,7 +421,7 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, mar
346
421
  </Text>
347
422
  ) : null}
348
423
  {displayDescription ? (
349
- <Text color={theme.text}>
424
+ <Text color={rowText}>
350
425
  {' '}
351
426
  {displayDescription}
352
427
  </Text>