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
@@ -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,
@@ -52,6 +52,9 @@ function hintStyle(tone) {
52
52
  // paddingX of 1, so the typing start can sit directly against that padding
53
53
  // without an extra guard column.
54
54
  const IME_LEFT_GUARD_COLUMNS = 0;
55
+ // Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
56
+ // needs to immediate-render). Matches transcript selection paint cadence.
57
+ const MOUSE_EXTEND_COALESCE_MS = 24;
55
58
 
56
59
  function insertText(draft, input) {
57
60
  if (!input) return draft;
@@ -93,14 +96,36 @@ function draftStateEqual(a, b) {
93
96
  );
94
97
  }
95
98
 
96
- 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) {
97
107
  const text = String(input ?? '');
98
108
  const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
99
109
  if (!body) return false;
100
110
  const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
101
- if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
111
+ if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
102
112
  const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
103
- 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));
104
129
  }
105
130
 
106
131
  export function PromptInput({
@@ -124,6 +149,7 @@ export function PromptInput({
124
149
  onRestoreQueued,
125
150
  onHistoryNavigate,
126
151
  onPasteText,
152
+ onVoiceToggle,
127
153
  selectionRef,
128
154
  valueRef,
129
155
  boxRectRef,
@@ -146,6 +172,7 @@ export function PromptInput({
146
172
  const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
147
173
  const contentWidthRef = useRef(80);
148
174
  const preferredColumnRef = useRef(null);
175
+ const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
149
176
  const { value, cursor } = draft;
150
177
  draftRef.current = draft;
151
178
  if (selectionRef) {
@@ -237,6 +264,49 @@ export function PromptInput({
237
264
  commitDraft(fn(draftRef.current), options);
238
265
  };
239
266
 
267
+ const cancelMouseExtendCoalesce = () => {
268
+ const state = mouseExtendCoalesceRef.current;
269
+ if (state.timer) {
270
+ clearTimeout(state.timer);
271
+ state.timer = null;
272
+ }
273
+ state.pendingNext = null;
274
+ };
275
+
276
+ const queueMouseExtendCommit = (next, immediate = false) => {
277
+ if (immediate) {
278
+ cancelMouseExtendCoalesce();
279
+ commitDraft(next);
280
+ mouseExtendCoalesceRef.current.t = Date.now();
281
+ return;
282
+ }
283
+ if (draftStateEqual(draftRef.current, next)) {
284
+ cancelMouseExtendCoalesce();
285
+ commitDraft(next);
286
+ return;
287
+ }
288
+ const state = mouseExtendCoalesceRef.current;
289
+ state.pendingNext = next;
290
+ const now = Date.now();
291
+ const elapsed = now - state.t;
292
+ if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
293
+ cancelMouseExtendCoalesce();
294
+ state.t = now;
295
+ commitDraft(next);
296
+ return;
297
+ }
298
+ if (state.timer) return;
299
+ state.timer = setTimeout(() => {
300
+ const current = mouseExtendCoalesceRef.current;
301
+ const pending = current.pendingNext;
302
+ current.timer = null;
303
+ current.pendingNext = null;
304
+ current.t = Date.now();
305
+ if (pending) commitDraft(pending);
306
+ }, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
307
+ state.timer.unref?.();
308
+ };
309
+
240
310
  // [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
241
311
  // click/drag cell over the prompt box to an edit offset and calls these so the
242
312
  // SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
@@ -246,22 +316,36 @@ export function PromptInput({
246
316
  mouseSelectionRef.current = {
247
317
  offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
248
318
  anchorAt: (offset) => {
319
+ cancelMouseExtendCoalesce();
249
320
  const value = draftRef.current.value;
250
321
  const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
251
322
  commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
323
+ mouseExtendCoalesceRef.current.t = Date.now();
252
324
  },
253
- extendTo: (offset) => {
325
+ extendTo: (offset, immediate = false) => {
326
+ if (offset == null) {
327
+ if (immediate) cancelMouseExtendCoalesce();
328
+ return;
329
+ }
254
330
  const d = draftRef.current;
255
331
  const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
256
332
  const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
257
- commitDraft({ ...d, cursor: off, selectionAnchor: anchor });
333
+ queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
258
334
  },
259
335
  clear: () => {
336
+ cancelMouseExtendCoalesce();
260
337
  if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
261
338
  },
262
339
  };
263
340
  }
264
341
 
342
+ useEffect(() => () => {
343
+ const state = mouseExtendCoalesceRef.current;
344
+ if (state.timer) clearTimeout(state.timer);
345
+ state.timer = null;
346
+ state.pendingNext = null;
347
+ }, []);
348
+
265
349
  const moveDraftVertically = (direction, { extend = false } = {}) => {
266
350
  const current = draftRef.current;
267
351
  const moved = verticalOffset(
@@ -375,6 +459,17 @@ export function PromptInput({
375
459
  return;
376
460
  }
377
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
+
378
473
  const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
379
474
  const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
380
475
  // Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
@@ -391,9 +486,33 @@ export function PromptInput({
391
486
  const lineBreakIndex = rawInput.search(/[\r\n]/);
392
487
  const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
393
488
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
394
- const rawCtrlEnter = isCtrlEnterSequence(rawInput);
489
+ const rawCtrlEnter = isModifiedEnterSequence(rawInput);
395
490
  const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
396
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
+
397
516
  const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
398
517
  if (pasteFallback) {
399
518
  handleExternalPaste(rawInput, { source: 'paste-fallback' });
@@ -658,6 +777,18 @@ export function PromptInput({
658
777
  return;
659
778
  }
660
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
+
661
792
  // Printable input (ignore other control keys). Strip any embedded SGR mouse
662
793
  // sequences as a belt-and-suspenders guard (the early return above catches
663
794
  // whole-sequence inputs; this removes partials that rode in with real text).
@@ -678,6 +809,12 @@ export function PromptInput({
678
809
  // resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
679
810
  // appear to land behind the caret (the observed scramble). Computing inside
680
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.
681
818
  cursorEnabledRef.current = !disabled && isRawModeSupported;
682
819
  installCursorAnchor();
683
820
 
@@ -699,8 +836,8 @@ export function PromptInput({
699
836
  const hintMeta = hintStyle(hintTone);
700
837
 
701
838
  return (
702
- <Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={theme.background}>
703
- <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()} />
704
841
  <Text color={theme.text} wrap="hard">{renderedValue}</Text>
705
842
  {!value && hint ? (
706
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>