mixdog 0.9.1 → 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 (214) hide show
  1. package/package.json +8 -1
  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/task-bench.mjs +207 -0
  23. package/scripts/tool-failures.mjs +6 -6
  24. package/scripts/tool-smoke.mjs +306 -66
  25. package/scripts/toolcall-args-test.mjs +81 -0
  26. package/src/agents/debugger/AGENT.md +4 -4
  27. package/src/agents/heavy-worker/AGENT.md +4 -2
  28. package/src/agents/reviewer/AGENT.md +4 -4
  29. package/src/agents/worker/AGENT.md +4 -2
  30. package/src/app.mjs +10 -6
  31. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  32. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  33. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  34. package/src/headless-role.mjs +14 -14
  35. package/src/help.mjs +1 -0
  36. package/src/lib/rules-builder.cjs +32 -54
  37. package/src/mixdog-session-runtime.mjs +710 -318
  38. package/src/output-styles/default.md +12 -7
  39. package/src/output-styles/minimal.md +25 -0
  40. package/src/output-styles/oneline.md +21 -0
  41. package/src/output-styles/simple.md +10 -9
  42. package/src/repl.mjs +12 -2
  43. package/src/rules/agent/00-common.md +7 -5
  44. package/src/rules/agent/30-explorer.md +7 -8
  45. package/src/rules/lead/01-general.md +3 -1
  46. package/src/rules/lead/lead-tool.md +7 -0
  47. package/src/rules/shared/01-tool.md +17 -12
  48. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  51. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  52. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  53. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  54. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  56. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  57. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  58. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  59. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  62. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
  64. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  65. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  66. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  67. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  69. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
  70. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
  71. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
  72. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  73. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  74. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
  76. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  77. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  78. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  80. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  82. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  83. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  84. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  85. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  87. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  90. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  95. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  99. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  100. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  101. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  102. package/src/runtime/channels/backends/discord.mjs +99 -9
  103. package/src/runtime/channels/backends/telegram.mjs +501 -0
  104. package/src/runtime/channels/index.mjs +441 -1224
  105. package/src/runtime/channels/lib/config.mjs +54 -2
  106. package/src/runtime/channels/lib/format.mjs +4 -2
  107. package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
  108. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  109. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  110. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  111. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  112. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  113. package/src/runtime/channels/lib/webhook.mjs +59 -31
  114. package/src/runtime/channels/tool-defs.mjs +1 -1
  115. package/src/runtime/memory/index.mjs +184 -19
  116. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  117. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  118. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  119. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  120. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  121. package/src/runtime/memory/lib/memory.mjs +101 -4
  122. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  123. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  124. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  125. package/src/runtime/memory/tool-defs.mjs +6 -3
  126. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  127. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  128. package/src/runtime/shared/config.mjs +9 -0
  129. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  130. package/src/runtime/shared/schedules-store.mjs +21 -19
  131. package/src/runtime/shared/tool-surface.mjs +98 -13
  132. package/src/runtime/shared/transcript-writer.mjs +129 -0
  133. package/src/runtime/shared/update-checker.mjs +214 -0
  134. package/src/standalone/agent-tool.mjs +255 -109
  135. package/src/standalone/channel-admin.mjs +133 -40
  136. package/src/standalone/channel-worker.mjs +8 -291
  137. package/src/standalone/explore-tool.mjs +2 -2
  138. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  139. package/src/standalone/provider-admin.mjs +11 -0
  140. package/src/standalone/usage-dashboard.mjs +1 -1
  141. package/src/tui/App.jsx +2096 -732
  142. package/src/tui/components/ConfirmBar.jsx +47 -0
  143. package/src/tui/components/ContextPanel.jsx +5 -3
  144. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  145. package/src/tui/components/Markdown.jsx +22 -98
  146. package/src/tui/components/Message.jsx +14 -35
  147. package/src/tui/components/Picker.jsx +87 -12
  148. package/src/tui/components/PromptInput.jsx +83 -7
  149. package/src/tui/components/QueuedCommands.jsx +1 -1
  150. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  151. package/src/tui/components/Spinner.jsx +7 -7
  152. package/src/tui/components/StatusLine.jsx +40 -21
  153. package/src/tui/components/TextEntryPanel.jsx +51 -7
  154. package/src/tui/components/ToolExecution.jsx +170 -98
  155. package/src/tui/components/TurnDone.jsx +4 -4
  156. package/src/tui/components/UsagePanel.jsx +1 -1
  157. package/src/tui/components/tool-output-format.mjs +159 -21
  158. package/src/tui/components/tool-output-format.test.mjs +87 -0
  159. package/src/tui/display-width.mjs +69 -0
  160. package/src/tui/display-width.test.mjs +35 -0
  161. package/src/tui/dist/index.mjs +6965 -2391
  162. package/src/tui/engine.mjs +287 -126
  163. package/src/tui/index.jsx +117 -7
  164. package/src/tui/keyboard-protocol.mjs +42 -0
  165. package/src/tui/lib/voice-recorder.mjs +453 -0
  166. package/src/tui/markdown/format-token.mjs +129 -76
  167. package/src/tui/markdown/format-token.test.mjs +61 -19
  168. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  169. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  170. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  171. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  172. package/src/tui/markdown/table-layout.mjs +9 -9
  173. package/src/tui/paste-attachments.mjs +0 -11
  174. package/src/tui/prompt-history-store.mjs +129 -0
  175. package/src/tui/prompt-history-store.test.mjs +52 -0
  176. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  177. package/src/tui/theme.mjs +41 -657
  178. package/src/tui/themes/base.mjs +86 -0
  179. package/src/tui/themes/basic.mjs +85 -0
  180. package/src/tui/themes/catppuccin.mjs +72 -0
  181. package/src/tui/themes/dracula.mjs +70 -0
  182. package/src/tui/themes/everforest.mjs +71 -0
  183. package/src/tui/themes/gruvbox.mjs +71 -0
  184. package/src/tui/themes/index.mjs +71 -0
  185. package/src/tui/themes/indigo.mjs +78 -0
  186. package/src/tui/themes/kanagawa.mjs +80 -0
  187. package/src/tui/themes/light.mjs +81 -0
  188. package/src/tui/themes/nord.mjs +72 -0
  189. package/src/tui/themes/onedark.mjs +16 -0
  190. package/src/tui/themes/rosepine.mjs +70 -0
  191. package/src/tui/themes/teal.mjs +81 -0
  192. package/src/tui/themes/tokyonight.mjs +79 -0
  193. package/src/tui/themes/utils.mjs +106 -0
  194. package/src/tui/themes/warm.mjs +79 -0
  195. package/src/tui/transcript-tool-failures.mjs +13 -2
  196. package/src/ui/markdown.mjs +1 -1
  197. package/src/ui/model-display.mjs +2 -2
  198. package/src/ui/statusline.mjs +26 -27
  199. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  200. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  201. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  202. package/src/workflows/default/WORKFLOW.md +39 -12
  203. package/src/workflows/sequential/WORKFLOW.md +46 -0
  204. package/src/workflows/solo/WORKFLOW.md +7 -0
  205. package/vendor/ink/build/display-width.js +62 -0
  206. package/vendor/ink/build/ink.js +100 -12
  207. package/vendor/ink/build/measure-text.js +4 -1
  208. package/vendor/ink/build/output.js +115 -9
  209. package/vendor/ink/build/render-node-to-output.js +4 -1
  210. package/vendor/ink/build/render.js +4 -0
  211. package/src/output-styles/extreme-simple.md +0 -20
  212. package/src/rules/lead/04-workflow.md +0 -51
  213. package/src/workflows/default/workflow.json +0 -13
  214. package/src/workflows/solo/workflow.json +0 -7
@@ -18,7 +18,7 @@
18
18
  import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
19
19
  import { Box, Text, useInput, usePaste, useStdin } from 'ink';
20
20
  import stringWidth from 'string-width';
21
- import { theme } from '../theme.mjs';
21
+ import { theme, surfaceBackground } from '../theme.mjs';
22
22
  import {
23
23
  caretPosition,
24
24
  clearSelection,
@@ -96,14 +96,36 @@ function draftStateEqual(a, b) {
96
96
  );
97
97
  }
98
98
 
99
- function isCtrlEnterSequence(input) {
99
+ // Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
100
+ // (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
101
+ // param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
102
+ // treat Ctrl+Enter AND Shift+Enter (the two common "insert newline" chords) as a
103
+ // newline, so we match when the shift OR ctrl bit is set. Ctrl+J is handled
104
+ // separately as the protocol-independent fallback.
105
+ const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
106
+ function isModifiedEnterSequence(input) {
100
107
  const text = String(input ?? '');
101
108
  const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
102
109
  if (!body) return false;
103
110
  const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
104
- if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
111
+ if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
105
112
  const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
106
- return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & 4) !== 0));
113
+ return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
114
+ }
115
+
116
+ // Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter \x1b[13;3u
117
+ // / \x1b[27;3;13~). Used to CONSUME modified-Enter sequences we don't map to a
118
+ // newline (Alt-only, etc.) so they aren't typed into the prompt as raw CSI text
119
+ // under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
120
+ // NOT matched, so it still submits.
121
+ function isAnyModifiedEnterSequence(input) {
122
+ const text = String(input ?? '');
123
+ const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
124
+ if (!body) return false;
125
+ const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
126
+ if (kitty) return (Number(kitty[1]) - 1) !== 0;
127
+ const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
128
+ return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
107
129
  }
108
130
 
109
131
  export function PromptInput({
@@ -127,6 +149,7 @@ export function PromptInput({
127
149
  onRestoreQueued,
128
150
  onHistoryNavigate,
129
151
  onPasteText,
152
+ onVoiceToggle,
130
153
  selectionRef,
131
154
  valueRef,
132
155
  boxRectRef,
@@ -436,6 +459,17 @@ export function PromptInput({
436
459
  return;
437
460
  }
438
461
 
462
+ // Safety net: drop CSI-private replies/fragments like \x1b[?<n>u / \x1b[?...c
463
+ // (escape may be stripped → `[?7u` / `[?1;0c`). We no longer query the
464
+ // terminal (enables are written unconditionally at raw-mode-on), so these
465
+ // should not normally appear — but a terminal that volunteers such a report
466
+ // must never type it into the prompt. The required `?` after `[` means this
467
+ // never matches a real kitty KEY event (those are \x1b[<codepoint>;<mods>u,
468
+ // no `?`); the optional final byte also discards any partial fragment.
469
+ if (/^(?:\x1b)?\[\?[\d;]*[uc]?$/.test(rawInput)) {
470
+ return;
471
+ }
472
+
439
473
  const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
440
474
  const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
441
475
  // Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
@@ -452,9 +486,33 @@ export function PromptInput({
452
486
  const lineBreakIndex = rawInput.search(/[\r\n]/);
453
487
  const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
454
488
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
455
- const rawCtrlEnter = isCtrlEnterSequence(rawInput);
489
+ const rawCtrlEnter = isModifiedEnterSequence(rawInput);
456
490
  const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
457
491
 
492
+ // Ctrl+J is the protocol-INDEPENDENT newline that works on every terminal.
493
+ // • Legacy / modifyOtherKeys terminals: Ctrl+J is a lone '\n' (0x0A). A real
494
+ // Enter is CR, which ink marks key.return (name 'return'); a lone '\n'
495
+ // arrives as name 'enter' with key.return false. A multi-char paste that
496
+ // contains '\n' is length > 1 (handled by the paste paths below).
497
+ // • Kitty protocol active: Ctrl+J arrives as \x1b[106;5u, which ink decodes
498
+ // to input 'j' with key.ctrl set.
499
+ // Either way → insert a newline. This MUST run before the trailing-newline/
500
+ // submit paths, since singleTrailingLineBreakPrefix('\n') returns '' (not
501
+ // null) and would otherwise route a bare Ctrl+J to submit.
502
+ if ((rawInput === '\n' && !key.return) || (key.ctrl && inputKey === 'j')) {
503
+ updateDraft((d) => replaceSelection(d, '\n'));
504
+ return;
505
+ }
506
+
507
+ // A modified Enter that is NOT a newline chord (e.g. Alt+Enter \x1b[13;3u or
508
+ // \x1b[27;3;13~). isModifiedEnterSequence already handled shift/ctrl above
509
+ // (→ newline); here we CONSUME any other modified Enter so its raw CSI bytes
510
+ // never type into the prompt under modifyOtherKeys. Plain Enter (mod=1) is
511
+ // not matched and still submits below.
512
+ if (!rawCtrlEnter && isAnyModifiedEnterSequence(rawInput)) {
513
+ return;
514
+ }
515
+
458
516
  const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
459
517
  if (pasteFallback) {
460
518
  handleExternalPaste(rawInput, { source: 'paste-fallback' });
@@ -719,6 +777,18 @@ export function PromptInput({
719
777
  return;
720
778
  }
721
779
 
780
+ // Ctrl+Space voice-record toggle. Two encodings observed across
781
+ // terminals: a lone NUL byte (legacy Ctrl+Space, many terminals send
782
+ // 0x00 directly) or the kitty CSI-u form `\x1b[32;5u` (codepoint 32
783
+ // = space, modifier 5 = 1+4 = ctrl). Consumed here — never typed into
784
+ // the prompt — and forwarded to the App-level recorder state machine
785
+ // via onVoiceToggle; a no-op when the prop isn't wired (e.g. embedded
786
+ // pickers) so this never throws.
787
+ if (rawInput === '\x00' || /^(?:\x1b)?\[32;5u$/.test(rawInput)) {
788
+ onVoiceToggle?.();
789
+ return;
790
+ }
791
+
722
792
  // Printable input (ignore other control keys). Strip any embedded SGR mouse
723
793
  // sequences as a belt-and-suspenders guard (the early return above catches
724
794
  // whole-sequence inputs; this removes partials that rode in with real text).
@@ -739,6 +809,12 @@ export function PromptInput({
739
809
  // resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
740
810
  // appear to land behind the caret (the observed scramble). Computing inside
741
811
  // the fork, from the real layout + current refs, fixes that by construction.
812
+ // Park the hardware cursor only when the prompt is a real, active edit target.
813
+ // The anchor stays enabled during an active turn too: suppressing it while
814
+ // the draft was empty (an earlier guard against a one-frame row-off paint
815
+ // when the transcript height changed mid-turn) made the cursor vanish for
816
+ // the whole turn after Enter, which reads as "the caret disappeared". A
817
+ // momentary one-frame drift is the lesser evil versus a missing caret.
742
818
  cursorEnabledRef.current = !disabled && isRawModeSupported;
743
819
  installCursorAnchor();
744
820
 
@@ -760,8 +836,8 @@ export function PromptInput({
760
836
  const hintMeta = hintStyle(hintTone);
761
837
 
762
838
  return (
763
- <Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={theme.background}>
764
- <Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={theme.background} />
839
+ <Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
840
+ <Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={surfaceBackground()} />
765
841
  <Text color={theme.text} wrap="hard">{renderedValue}</Text>
766
842
  {!value && hint ? (
767
843
  <Box marginLeft={-1}>
@@ -22,7 +22,7 @@ export function QueuedCommands({ queued, columns }) {
22
22
  // Windows auto-wrap/scroll that drifts the alt-screen frame (see UserMessage).
23
23
  const bandColumns = Math.max(1, columns - 1);
24
24
  return (
25
- <Box marginTop={1} flexDirection="column">
25
+ <Box flexDirection="column">
26
26
  {queued.map((item) => {
27
27
  // Truncate to 1 line so the row reservation (queued.length in App.jsx)
28
28
  // stays accurate — wrapped text would push the input box off-screen.
@@ -65,6 +65,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
65
65
  const descriptionWidth = Math.max(0, columns - labelWidth - 12);
66
66
  // Standard panel rhythm: title row, blank, description/hint row, blank, content.
67
67
  const description = truncateText(SLASH_DESCRIPTION, Math.max(0, columns - 4));
68
+ const titleWidth = stringWidth(String(title || ''));
69
+ const help = truncateText(SLASH_HELP, Math.max(0, columns - titleWidth - 7));
68
70
 
69
71
  return (
70
72
  <Box flexDirection="column" flexShrink={0} width="100%">
@@ -76,8 +78,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
76
78
  width="100%"
77
79
  >
78
80
  <Box flexDirection="row" justifyContent="space-between">
79
- <Text color={theme.panelTitle}>{title}</Text>
80
- <Text color={theme.subtle}>{SLASH_HELP}</Text>
81
+ <Text color={theme.panelTitle} wrap="truncate">{title}</Text>
82
+ <Text color={theme.subtle} wrap="truncate">{help}</Text>
81
83
  </Box>
82
84
  <Text> </Text>
83
85
  <Text color={theme.subtle}>{description || ' '}</Text>
@@ -103,14 +105,15 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
103
105
  const CommandRow = React.memo(function CommandRow({ command, isSelected, labelWidth, descriptionWidth, query }) {
104
106
  const label = truncateText(commandDisplayLabel(command, query), labelWidth);
105
107
  const description = truncateText(command.description, descriptionWidth);
108
+ const rowText = isSelected ? theme.selectionText : theme.text;
106
109
 
107
110
  return (
108
- <Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.userMessageBackground : undefined}>
109
- <Text color={theme.text}>
111
+ <Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.selectionBackground : undefined}>
112
+ <Text color={rowText}>
110
113
  {padCells(label, labelWidth)}
111
114
  </Text>
112
115
  {description ? (
113
- <Text color={theme.text}>
116
+ <Text color={rowText}>
114
117
  {' '}
115
118
  {description}
116
119
  </Text>
@@ -58,8 +58,6 @@ const MODE_VERBS = {
58
58
  responding: ['Responding', 'Composing', 'Writing', 'Wrapping up'],
59
59
  };
60
60
 
61
- const ERROR_RED = { r: 171, g: 43, b: 63 };
62
-
63
61
  function interpolateColor(a, b, t) {
64
62
  return {
65
63
  r: Math.round(a.r + (b.r - a.r) * t),
@@ -117,6 +115,7 @@ function spinnerRgb() {
117
115
  SPINNER_GLYPH_RGB: parseRgb(theme.spinnerGlyph) ?? { r: 240, g: 240, b: 240 },
118
116
  THINKING_INACTIVE: parseRgb(theme.thinkingBase) ?? parseRgb(theme.thinkingAccent) ?? { r: 153, g: 153, b: 153 },
119
117
  THINKING_SHIMMER: parseRgb(theme.thinkingGlow) ?? { r: 255, g: 205, b: 175 },
118
+ STALL_RGB: parseRgb(theme.error) ?? { r: 171, g: 43, b: 63 },
120
119
  };
121
120
  }
122
121
 
@@ -172,7 +171,7 @@ function tokenModeGlyph(mode) {
172
171
 
173
172
  export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, mode = 'responding', columns = 80, marginTop = 1 }) {
174
173
  useAnimation({ interval: FRAME_MS });
175
- const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER } = spinnerRgb();
174
+ const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER, STALL_RGB } = spinnerRgb();
176
175
  const now = Date.now();
177
176
  const elapsedMs = startedAt ? Math.max(0, now - startedAt) : 0;
178
177
  const frame = Math.floor(elapsedMs / FRAME_MS);
@@ -222,17 +221,18 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
222
221
  const glyphColor = stalledIntensity > 0
223
222
  ? toRgbString(interpolateColor(
224
223
  SPINNER_GLYPH_RGB,
225
- ERROR_RED,
224
+ STALL_RGB,
226
225
  stalledIntensity
227
226
  ))
228
227
  : theme.spinnerGlyph;
229
228
 
230
229
  // --- Verb shimmer (traveling highlight) ---
231
- if (displayVerbModeRef.current !== mode || !displayVerbRef.current) {
232
- displayVerbModeRef.current = mode;
230
+ if (!displayVerbRef.current) {
233
231
  displayVerbRef.current = stableModeVerb(mode, verb);
234
232
  nextVerbCheckRef.current = nextVerbCheckAt(now);
235
- } else if (now >= nextVerbCheckRef.current) {
233
+ }
234
+ displayVerbModeRef.current = mode;
235
+ if (now >= nextVerbCheckRef.current) {
236
236
  displayVerbRef.current = chooseNextVerb(mode, verb, displayVerbRef.current);
237
237
  nextVerbCheckRef.current = nextVerbCheckAt(now);
238
238
  }
@@ -9,7 +9,7 @@
9
9
  import React, { useEffect, useRef, useState } from 'react';
10
10
  import { Box, Text } from 'ink';
11
11
  import { displayModelName, shortenModelName } from '../../ui/model-display.mjs';
12
- import { theme } from '../theme.mjs';
12
+ import { theme, surfaceBackground } from '../theme.mjs';
13
13
  import {
14
14
  normalizeStatuslineAnsi,
15
15
  statuslineFooterCacheKey,
@@ -33,6 +33,20 @@ function resetStatuslineModuleLoad() {
33
33
 
34
34
  const RESET = '\x1b[0m';
35
35
  const STATUSLINE_RENDER_DEBOUNCE_MS = 150;
36
+ /** Background import of statusline.mjs before the +1000ms full-render tick. */
37
+ const STATUSLINE_MODULE_PREWARM_DELAY_MS = 300;
38
+
39
+ let statuslineModulePrewarmScheduled = false;
40
+
41
+ function scheduleStatuslineModulePrewarm() {
42
+ if (statuslineModulePrewarmScheduled) return;
43
+ statuslineModulePrewarmScheduled = true;
44
+ const timer = setTimeout(() => {
45
+ loadStatuslineModule().catch(() => {});
46
+ }, STATUSLINE_MODULE_PREWARM_DELAY_MS);
47
+ timer.unref?.();
48
+ }
49
+
36
50
  const STATUSLINE_BOOT_FULL_DELAY_MS = 1000;
37
51
  const STATUSLINE_BOOT_FULL_DELAY_ACTIVE_MS = 2200;
38
52
  const STATUSLINE_BOOT_FULL_RETRY_MS = 2000;
@@ -46,7 +60,7 @@ function isTerminalStatus(statusText) {
46
60
 
47
61
  function hasRunningStatuslineWorkers(agentWorkers = [], agentJobs = []) {
48
62
  for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
49
- const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
63
+ const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
50
64
  if (tag && !isTerminalStatus(worker?.stage || worker?.status)) return true;
51
65
  }
52
66
  for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
@@ -143,8 +157,8 @@ function localContextPct({
143
157
  : (localNum(contextWindow) > 0
144
158
  ? localNum(contextWindow)
145
159
  : (localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 200_000)));
146
- const compactTrigger = localNum(autoCompactTokenLimit);
147
- const window = compactTrigger > 0 && compactTrigger < baseWindow ? compactTrigger : baseWindow;
160
+ // autoCompactTokenLimit is the compaction trigger only; pct uses the boundary window.
161
+ const window = baseWindow;
148
162
  const s = stats && typeof stats === 'object' ? stats : {};
149
163
  const source = String(s.currentContextSource || '').toLowerCase();
150
164
  const estimated = localNum(s.currentEstimatedContextTokens);
@@ -190,11 +204,6 @@ const LOCAL_WORKER_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '
190
204
  // render stay in sync (no flicker).
191
205
  const LOCAL_L2_SPINNER_FRAME_MS = 120;
192
206
 
193
- function localWorkerSpinnerFrame(now = Date.now()) {
194
- const index = Math.floor(now / 160) % LOCAL_WORKER_SPINNER_FRAMES.length;
195
- return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
196
- }
197
-
198
207
  function localL2SpinnerFrame(now = Date.now()) {
199
208
  const index = Math.floor(now / LOCAL_L2_SPINNER_FRAME_MS) % LOCAL_WORKER_SPINNER_FRAMES.length;
200
209
  return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
@@ -224,13 +233,13 @@ function localFormatElapsed(ms) {
224
233
  function localRunningWorkerCount(agentWorkers = [], agentJobs = []) {
225
234
  const seen = new Set();
226
235
  for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
227
- const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
236
+ const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
228
237
  if (!tag || isTerminalStatus(worker?.stage || worker?.status)) continue;
229
238
  seen.add(tag);
230
239
  }
231
240
  for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
232
241
  if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
233
- const tag = String(job?.tag || job?.role || job?.type || job?.task_id || job?.taskId || '').trim();
242
+ const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || '').trim();
234
243
  if (!tag) continue;
235
244
  seen.add(tag);
236
245
  }
@@ -241,14 +250,14 @@ function localRunningWorkerTags(agentWorkers = [], agentJobs = [], limit = 3) {
241
250
  const tags = [];
242
251
  const seen = new Set();
243
252
  for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
244
- const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
253
+ const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
245
254
  if (!tag || isTerminalStatus(worker?.stage || worker?.status) || seen.has(tag)) continue;
246
255
  seen.add(tag);
247
256
  tags.push(tag);
248
257
  }
249
258
  for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
250
259
  if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
251
- const tag = String(job?.tag || job?.role || job?.type || job?.task_id || job?.taskId || '').trim();
260
+ const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || '').trim();
252
261
  if (!tag || seen.has(tag)) continue;
253
262
  seen.add(tag);
254
263
  tags.push(tag);
@@ -417,12 +426,13 @@ export function normalizeStatusLine(text) {
417
426
  return normalizeStatuslineAnsi(text, statusColors(), { reset: RESET });
418
427
  }
419
428
 
420
- function workflowModeLabel(workflow = {}) {
429
+ function workflowModeLabel(workflow = {}, remoteEnabled = false) {
421
430
  const name = String(workflow?.name || workflow?.id || 'Default').trim() || 'Default';
422
- return `${name} Mode`;
431
+ const base = `${name} Mode`;
432
+ return remoteEnabled === true ? `Remote / ${base}` : base;
423
433
  }
424
434
 
425
- function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0, rawContextWindow, resizeEpoch, agentRevision = '', agentWorkers = [], agentJobs = [], activeTools = null, initialLine = '', workflow = null, themeEpoch = 0 }) {
435
+ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0, rawContextWindow, resizeEpoch, agentRevision = '', agentWorkers = [], agentJobs = [], activeTools = null, initialLine = '', workflow = null, remoteEnabled = false, themeEpoch = 0 }) {
426
436
  const [line, setLine] = useState(() => normalizeStatusLine(initialLine || localBootStatusLine({
427
437
  provider,
428
438
  model,
@@ -474,6 +484,10 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
474
484
  : '';
475
485
  const refreshMs = hasActiveStatuslineWork(line, agentWorkers, agentJobs, activeTools) ? STATUSLINE_ACTIVE_REFRESH_MS : STATUSLINE_REFRESH_MS;
476
486
 
487
+ useEffect(() => {
488
+ scheduleStatuslineModulePrewarm();
489
+ }, []);
490
+
477
491
  useEffect(() => {
478
492
  const timer = setInterval(() => {
479
493
  setRefreshTick((tick) => (tick + 1) % 1_000_000);
@@ -634,11 +648,12 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
634
648
  }, [sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkers, agentJobs, activeToolsSignature, refreshTick, themeEpoch]);
635
649
 
636
650
  const lines = line ? line.split('\n').slice(0, 2) : [' ', ' '];
637
- const workflowLabel = workflowModeLabel(workflow);
638
- // Footer footprint stays 3 rows total, but L2 sits directly under L1 without
639
- // an internal spacer; the remaining row is kept as outer breathing room.
651
+ const workflowLabel = workflowModeLabel(workflow, remoteEnabled);
652
+ // Footer footprint stays 3 rows total (L1 + L2 + one spacer). Keep L1/L2 in
653
+ // the TOP two rows so the statusline is visually attached to the prompt box;
654
+ // the leftover breathing row sits below L2 at the terminal bottom.
640
655
  return (
641
- <Box flexDirection="column" width="100%" height={3} overflow="hidden" paddingLeft={2} backgroundColor={theme.background}>
656
+ <Box flexDirection="column" width="100%" height={3} overflow="hidden" justifyContent="flex-start" paddingLeft={2} backgroundColor={surfaceBackground()}>
642
657
  <Box flexDirection="row" width="100%" overflow="hidden">
643
658
  <Box flexGrow={1} flexShrink={1} overflow="hidden">
644
659
  <Text wrap="truncate">{lines[0] || ' '}</Text>
@@ -647,7 +662,11 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
647
662
  <Text color={theme.statusText} wrap="truncate">{workflowLabel}</Text>
648
663
  </Box>
649
664
  </Box>
650
- <Text wrap="truncate">{lines[1] || ' '}</Text>
665
+ <Box flexDirection="row" width="100%" overflow="hidden">
666
+ <Box flexGrow={1} flexShrink={1} overflow="hidden">
667
+ <Text wrap="truncate">{lines[1] || ' '}</Text>
668
+ </Box>
669
+ </Box>
651
670
  </Box>
652
671
  );
653
672
  }
@@ -4,7 +4,7 @@
4
4
  import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
5
5
  import { Box, Text, useInput, usePaste, useStdin } from 'ink';
6
6
  import stringWidth from 'string-width';
7
- import { theme } from '../theme.mjs';
7
+ import { theme, surfaceBackground } from '../theme.mjs';
8
8
  import {
9
9
  clearSelection,
10
10
  deleteBackwardWord,
@@ -126,14 +126,34 @@ function singleTrailingLineBreakPrefix(text) {
126
126
  return prefix.includes('\n') ? null : prefix;
127
127
  }
128
128
 
129
- function isCtrlEnterSequence(input) {
129
+ // Recognize a MODIFIED Enter (Ctrl+Enter or Shift+Enter) delivered via the kitty
130
+ // keyboard protocol (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The
131
+ // xterm modifier param is (1 + bitmask): shift=1, alt=2, ctrl=4. We match when
132
+ // the shift OR ctrl bit is set so both chords insert a newline. Ctrl+J is the
133
+ // protocol-independent fallback, handled separately.
134
+ const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
135
+ function isModifiedEnterSequence(input) {
130
136
  const text = String(input ?? '');
131
137
  const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
132
138
  if (!body) return false;
133
139
  const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
134
- if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
140
+ if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
135
141
  const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
136
- return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & 4) !== 0));
142
+ return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
143
+ }
144
+
145
+ // Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter). Used to
146
+ // CONSUME modified-Enter sequences we don't map to a newline so they aren't
147
+ // typed as raw CSI text under modifyOtherKeys. Plain Enter (mod=1) is NOT
148
+ // matched, so it still submits.
149
+ function isAnyModifiedEnterSequence(input) {
150
+ const text = String(input ?? '');
151
+ const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
152
+ if (!body) return false;
153
+ const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
154
+ if (kitty) return (Number(kitty[1]) - 1) !== 0;
155
+ const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
156
+ return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
137
157
  }
138
158
 
139
159
  export function TextEntryPanel({
@@ -227,6 +247,11 @@ export function TextEntryPanel({
227
247
  const rawSource = String(input ?? '');
228
248
  const rawInput = normalizeInput(input);
229
249
  if (/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/.test(rawSource)) return;
250
+ // Safety net: drop CSI-private replies/fragments (\x1b[?<n>u / \x1b[?...c).
251
+ // We no longer query the terminal, so these should not normally appear, but
252
+ // a volunteered report must never type into the field. See PromptInput for
253
+ // the full rationale; optional final byte also discards partial fragments.
254
+ if (/^(?:\x1b)?\[\?[\d;]*[uc]?$/.test(rawSource)) return;
230
255
 
231
256
  if (key.escape) {
232
257
  if (selectionRange(draftRef.current)) {
@@ -237,8 +262,27 @@ export function TextEntryPanel({
237
262
  return;
238
263
  }
239
264
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
240
- const rawCtrlEnter = isCtrlEnterSequence(rawSource) || isCtrlEnterSequence(rawInput);
265
+ const rawCtrlEnter = isModifiedEnterSequence(rawSource) || isModifiedEnterSequence(rawInput);
241
266
  const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
267
+
268
+ // Ctrl+J — the protocol-independent newline that works on every terminal.
269
+ // Legacy/modifyOtherKeys: a lone '\n' (real Enter is CR → key.return). Kitty:
270
+ // \x1b[106;5u → input 'j' with key.ctrl. Either → insert a newline. Must
271
+ // precede the trailing-newline/submit paths since
272
+ // singleTrailingLineBreakPrefix('\n') returns '' (not null) and would
273
+ // otherwise route Ctrl+J to submit.
274
+ if ((rawSource === '\n' && !key.return) || (key.ctrl && rawSource.toLowerCase() === 'j')) {
275
+ updateDraft((d) => replaceSelection(d, '\n'));
276
+ return;
277
+ }
278
+
279
+ // A modified Enter that is NOT a newline chord (e.g. Alt+Enter): consume it
280
+ // so its raw CSI bytes don't type into the field under modifyOtherKeys. Plain
281
+ // Enter (mod=1) is not matched and still submits below.
282
+ if (!rawCtrlEnter && (isAnyModifiedEnterSequence(rawSource) || isAnyModifiedEnterSequence(rawInput))) {
283
+ return;
284
+ }
285
+
242
286
  const pasteFallback = rawInput.includes('\n') && trailingEnterPrefix === null && (rawInput.length > 1 || !key.return);
243
287
  if (pasteFallback) {
244
288
  updateDraft((d) => insertText(d, rawInput));
@@ -256,7 +300,7 @@ export function TextEntryPanel({
256
300
  updateDraft((d) => replaceSelection(d, '\n'));
257
301
  return;
258
302
  }
259
- if (key.return || rawInput === '\n') {
303
+ if (key.return) {
260
304
  if (modifiedLineBreak) {
261
305
  updateDraft((d) => replaceSelection(d, '\n'));
262
306
  return;
@@ -432,7 +476,7 @@ export function TextEntryPanel({
432
476
  <Text> </Text>
433
477
  <Text color={theme.subtle}>{hintText || ' '}</Text>
434
478
  <Text> </Text>
435
- <Box ref={boxRef} flexDirection="row" width="100%" backgroundColor={theme.background}>
479
+ <Box ref={boxRef} flexDirection="row" width="100%" backgroundColor={surfaceBackground()}>
436
480
  <Text color={theme.inactive}>{promptLabel}</Text>
437
481
  <Text color={theme.text} wrap="truncate">{renderedValue}</Text>
438
482
  </Box>