@zhijiewang/openharness 2.1.0 → 2.3.1

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 (231) hide show
  1. package/README.md +4 -4
  2. package/dist/DeferredTool.js +3 -1
  3. package/dist/Tool.d.ts +1 -1
  4. package/dist/agents/roles.js +58 -62
  5. package/dist/commands/cybergotchi.d.ts +1 -1
  6. package/dist/commands/cybergotchi.js +30 -30
  7. package/dist/commands/index.js +288 -132
  8. package/dist/components/App.d.ts +1 -1
  9. package/dist/components/App.js +6 -6
  10. package/dist/components/CompanionFooter.d.ts +1 -1
  11. package/dist/components/CompanionFooter.js +6 -8
  12. package/dist/components/CybergotchiBubble.js +5 -5
  13. package/dist/components/CybergotchiPanel.d.ts +1 -1
  14. package/dist/components/CybergotchiPanel.js +7 -7
  15. package/dist/components/CybergotchiPanelConnected.js +2 -2
  16. package/dist/components/CybergotchiSetup.js +26 -24
  17. package/dist/components/CybergotchiSprite.d.ts +1 -1
  18. package/dist/components/CybergotchiSprite.js +8 -12
  19. package/dist/components/DiffView.d.ts +1 -1
  20. package/dist/components/DiffView.js +10 -10
  21. package/dist/components/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/ErrorBoundary.js +1 -1
  23. package/dist/components/InitWizard.js +65 -33
  24. package/dist/components/Markdown.js +2 -4
  25. package/dist/components/Messages.js +4 -4
  26. package/dist/components/PermissionPrompt.d.ts +1 -1
  27. package/dist/components/PermissionPrompt.js +15 -17
  28. package/dist/components/REPL.d.ts +1 -1
  29. package/dist/components/REPL.js +74 -49
  30. package/dist/components/Spinner.js +2 -2
  31. package/dist/components/TextInput.js +35 -29
  32. package/dist/components/ToolCallDisplay.js +3 -5
  33. package/dist/cybergotchi/bones.d.ts +1 -1
  34. package/dist/cybergotchi/bones.js +8 -8
  35. package/dist/cybergotchi/config.d.ts +2 -2
  36. package/dist/cybergotchi/config.js +13 -13
  37. package/dist/cybergotchi/events.d.ts +5 -5
  38. package/dist/cybergotchi/events.js +7 -7
  39. package/dist/cybergotchi/needs.d.ts +2 -2
  40. package/dist/cybergotchi/needs.js +7 -9
  41. package/dist/cybergotchi/personality.d.ts +2 -2
  42. package/dist/cybergotchi/personality.js +2 -2
  43. package/dist/cybergotchi/species.d.ts +1 -1
  44. package/dist/cybergotchi/species.js +145 -217
  45. package/dist/cybergotchi/speech.d.ts +2 -2
  46. package/dist/cybergotchi/speech.js +43 -43
  47. package/dist/cybergotchi/types.d.ts +4 -4
  48. package/dist/cybergotchi/types.js +26 -26
  49. package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
  50. package/dist/cybergotchi/useCybergotchi.js +29 -25
  51. package/dist/git/index.js +11 -9
  52. package/dist/harness/checkpoints.js +29 -21
  53. package/dist/harness/config.d.ts +3 -3
  54. package/dist/harness/config.js +15 -9
  55. package/dist/harness/context-warning.d.ts +1 -1
  56. package/dist/harness/context-warning.js +1 -1
  57. package/dist/harness/cost.js +1 -1
  58. package/dist/harness/credentials.js +13 -13
  59. package/dist/harness/hooks.js +7 -5
  60. package/dist/harness/keybindings.js +20 -18
  61. package/dist/harness/marketplace.d.ts +3 -3
  62. package/dist/harness/marketplace.js +55 -42
  63. package/dist/harness/memory.d.ts +23 -5
  64. package/dist/harness/memory.js +142 -41
  65. package/dist/harness/onboarding.js +30 -10
  66. package/dist/harness/plugins.d.ts +9 -1
  67. package/dist/harness/plugins.js +54 -30
  68. package/dist/harness/rules.js +12 -7
  69. package/dist/harness/sandbox.js +15 -15
  70. package/dist/harness/session-db.d.ts +55 -0
  71. package/dist/harness/session-db.js +165 -0
  72. package/dist/harness/session.d.ts +1 -1
  73. package/dist/harness/session.js +34 -15
  74. package/dist/harness/store.d.ts +3 -3
  75. package/dist/harness/store.js +6 -4
  76. package/dist/harness/submit-handler.d.ts +4 -4
  77. package/dist/harness/submit-handler.js +25 -23
  78. package/dist/harness/telemetry.d.ts +1 -1
  79. package/dist/harness/telemetry.js +23 -19
  80. package/dist/harness/traces.d.ts +2 -2
  81. package/dist/harness/traces.js +39 -33
  82. package/dist/harness/verification.d.ts +1 -1
  83. package/dist/harness/verification.js +50 -44
  84. package/dist/lsp/client.js +44 -40
  85. package/dist/main.js +114 -59
  86. package/dist/mcp/DeferredMcpTool.d.ts +4 -4
  87. package/dist/mcp/DeferredMcpTool.js +9 -5
  88. package/dist/mcp/McpTool.d.ts +4 -4
  89. package/dist/mcp/McpTool.js +8 -4
  90. package/dist/mcp/client.d.ts +2 -2
  91. package/dist/mcp/client.js +21 -21
  92. package/dist/mcp/loader.d.ts +1 -1
  93. package/dist/mcp/loader.js +17 -12
  94. package/dist/mcp/registry.d.ts +3 -3
  95. package/dist/mcp/registry.js +97 -97
  96. package/dist/mcp/schema.d.ts +1 -1
  97. package/dist/mcp/schema.js +16 -16
  98. package/dist/mcp/server.d.ts +1 -1
  99. package/dist/mcp/server.js +21 -21
  100. package/dist/mcp/types.d.ts +3 -3
  101. package/dist/providers/anthropic.d.ts +2 -2
  102. package/dist/providers/anthropic.js +10 -9
  103. package/dist/providers/base.d.ts +1 -1
  104. package/dist/providers/index.js +10 -3
  105. package/dist/providers/llamacpp.d.ts +2 -2
  106. package/dist/providers/llamacpp.js +1 -3
  107. package/dist/providers/ollama.d.ts +2 -2
  108. package/dist/providers/ollama.js +3 -4
  109. package/dist/providers/openai.d.ts +2 -2
  110. package/dist/providers/openai.js +3 -5
  111. package/dist/providers/openrouter.d.ts +2 -2
  112. package/dist/providers/router.d.ts +1 -1
  113. package/dist/providers/router.js +7 -7
  114. package/dist/query/compress.d.ts +2 -2
  115. package/dist/query/compress.js +22 -21
  116. package/dist/query/context-manager.d.ts +1 -1
  117. package/dist/query/context-manager.js +5 -5
  118. package/dist/query/errors.js +1 -1
  119. package/dist/query/index.d.ts +1 -1
  120. package/dist/query/index.js +42 -24
  121. package/dist/query/tools.js +15 -12
  122. package/dist/query/types.d.ts +3 -1
  123. package/dist/query.d.ts +1 -1
  124. package/dist/query.js +1 -1
  125. package/dist/remote/auth.d.ts +2 -2
  126. package/dist/remote/auth.js +8 -8
  127. package/dist/remote/server.d.ts +3 -3
  128. package/dist/remote/server.js +60 -60
  129. package/dist/renderer/cells.js +9 -9
  130. package/dist/renderer/colors.js +24 -6
  131. package/dist/renderer/diff.d.ts +2 -2
  132. package/dist/renderer/diff.js +27 -19
  133. package/dist/renderer/differ.d.ts +1 -1
  134. package/dist/renderer/differ.js +9 -9
  135. package/dist/renderer/image.js +19 -19
  136. package/dist/renderer/index.d.ts +6 -6
  137. package/dist/renderer/index.js +163 -93
  138. package/dist/renderer/input.js +66 -48
  139. package/dist/renderer/layout.d.ts +6 -6
  140. package/dist/renderer/layout.js +163 -124
  141. package/dist/renderer/markdown.d.ts +2 -2
  142. package/dist/renderer/markdown.js +173 -54
  143. package/dist/renderer/session-browser.d.ts +2 -2
  144. package/dist/renderer/session-browser.js +19 -21
  145. package/dist/repl.d.ts +5 -5
  146. package/dist/repl.js +311 -198
  147. package/dist/sdk/index.d.ts +5 -5
  148. package/dist/sdk/index.js +32 -26
  149. package/dist/services/AgentDispatcher.d.ts +3 -3
  150. package/dist/services/AgentDispatcher.js +33 -29
  151. package/dist/services/CronExecutor.d.ts +4 -4
  152. package/dist/services/CronExecutor.js +12 -8
  153. package/dist/services/EvaluatorLoop.d.ts +3 -3
  154. package/dist/services/EvaluatorLoop.js +29 -21
  155. package/dist/services/MetaHarness.d.ts +1 -1
  156. package/dist/services/MetaHarness.js +34 -32
  157. package/dist/services/PipelineExecutor.d.ts +1 -1
  158. package/dist/services/PipelineExecutor.js +23 -25
  159. package/dist/services/SkillExtractor.d.ts +43 -0
  160. package/dist/services/SkillExtractor.js +163 -0
  161. package/dist/services/StreamingToolExecutor.d.ts +2 -2
  162. package/dist/services/StreamingToolExecutor.js +11 -7
  163. package/dist/services/a2a.d.ts +8 -8
  164. package/dist/services/a2a.js +44 -34
  165. package/dist/services/agent-messaging.d.ts +33 -15
  166. package/dist/services/agent-messaging.js +65 -13
  167. package/dist/services/cron.js +16 -16
  168. package/dist/tools/AgentTool/index.d.ts +5 -2
  169. package/dist/tools/AgentTool/index.js +25 -39
  170. package/dist/tools/AskUserTool/index.js +1 -1
  171. package/dist/tools/BashTool/index.d.ts +2 -2
  172. package/dist/tools/BashTool/index.js +18 -10
  173. package/dist/tools/CronTool/index.js +30 -12
  174. package/dist/tools/DiagnosticsTool/index.js +28 -22
  175. package/dist/tools/EnterPlanModeTool/index.js +93 -14
  176. package/dist/tools/EnterWorktreeTool/index.js +7 -3
  177. package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
  178. package/dist/tools/ExitPlanModeTool/index.js +20 -5
  179. package/dist/tools/ExitWorktreeTool/index.js +11 -4
  180. package/dist/tools/FileEditTool/index.js +3 -5
  181. package/dist/tools/FileReadTool/index.js +16 -10
  182. package/dist/tools/FileWriteTool/index.js +2 -2
  183. package/dist/tools/GlobTool/index.js +5 -9
  184. package/dist/tools/GrepTool/index.d.ts +2 -2
  185. package/dist/tools/GrepTool/index.js +14 -9
  186. package/dist/tools/ImageReadTool/index.js +2 -2
  187. package/dist/tools/KillProcessTool/index.js +11 -7
  188. package/dist/tools/LSTool/index.js +3 -3
  189. package/dist/tools/MemoryTool/index.d.ts +5 -5
  190. package/dist/tools/MemoryTool/index.js +28 -14
  191. package/dist/tools/MonitorTool/index.js +24 -19
  192. package/dist/tools/MultiEditTool/index.js +9 -5
  193. package/dist/tools/NotebookEditTool/index.js +3 -3
  194. package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
  195. package/dist/tools/ParallelAgentTool/index.js +12 -6
  196. package/dist/tools/PipelineTool/index.js +3 -3
  197. package/dist/tools/PowerShellTool/index.js +10 -6
  198. package/dist/tools/RemoteTriggerTool/index.js +8 -4
  199. package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
  200. package/dist/tools/ScheduleWakeupTool/index.js +115 -0
  201. package/dist/tools/SendMessageTool/index.js +25 -7
  202. package/dist/tools/SessionSearchTool/index.d.ts +15 -0
  203. package/dist/tools/SessionSearchTool/index.js +36 -0
  204. package/dist/tools/SkillTool/index.d.ts +3 -0
  205. package/dist/tools/SkillTool/index.js +39 -9
  206. package/dist/tools/TaskCreateTool/index.d.ts +2 -2
  207. package/dist/tools/TaskCreateTool/index.js +2 -2
  208. package/dist/tools/TaskGetTool/index.js +2 -2
  209. package/dist/tools/TaskListTool/index.js +3 -5
  210. package/dist/tools/TaskOutputTool/index.js +2 -2
  211. package/dist/tools/TaskStopTool/index.js +3 -3
  212. package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
  213. package/dist/tools/TaskUpdateTool/index.js +2 -2
  214. package/dist/tools/ToolSearchTool/index.js +9 -6
  215. package/dist/tools/WebFetchTool/index.js +1 -1
  216. package/dist/tools/WebSearchTool/index.js +2 -6
  217. package/dist/tools.js +31 -30
  218. package/dist/types/permissions.js +15 -9
  219. package/dist/utils/bash-safety.d.ts +1 -1
  220. package/dist/utils/bash-safety.js +64 -54
  221. package/dist/utils/diff-algorithm.d.ts +3 -3
  222. package/dist/utils/diff-algorithm.js +7 -7
  223. package/dist/utils/fs.js +3 -3
  224. package/dist/utils/safe-env.js +1 -1
  225. package/dist/utils/theme-data.d.ts +1 -1
  226. package/dist/utils/theme-data.js +1 -1
  227. package/dist/utils/theme.d.ts +1 -1
  228. package/dist/utils/theme.js +1 -1
  229. package/dist/utils/tool-summary.d.ts +1 -1
  230. package/dist/utils/tool-summary.js +27 -9
  231. package/package.json +10 -3
@@ -2,23 +2,23 @@
2
2
  * Layout engine — rasterizes application state into a CellGrid.
3
3
  * Split screen: messages area (top) + footer (bottom).
4
4
  */
5
- import { renderMarkdown, measureMarkdown } from './markdown.js';
6
- import { renderDiff } from './diff.js';
7
- import { isImageOutput, renderImageInline } from './image.js';
8
- import { renderSessionBrowser } from './session-browser.js';
9
- import { getTheme } from '../utils/theme-data.js';
5
+ import { getTheme } from "../utils/theme-data.js";
6
+ import { renderDiff } from "./diff.js";
7
+ import { isImageOutput, renderImageInline } from "./image.js";
8
+ import { measureMarkdown, renderMarkdown } from "./markdown.js";
9
+ import { renderSessionBrowser } from "./session-browser.js";
10
10
  // ── Style constants ──
11
11
  const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
12
12
  const S_TEXT = s(null);
13
13
  const S_DIM = s(null, false, true);
14
14
  const S_BORDER = s(null, false, true);
15
15
  const S_BRIGHT = s(null);
16
- const S_BANNER = s('cyan');
16
+ const S_BANNER = s("cyan");
17
17
  const S_BANNER_DIM = s(null, false, true);
18
- const S_AGENT = s('cyan', true);
19
- const S_KEY_GREEN = s('green', true);
20
- const S_KEY_RED = s('red', true);
21
- const S_KEY_CYAN = s('cyan', true);
18
+ const S_AGENT = s("cyan", true);
19
+ const S_KEY_GREEN = s("green", true);
20
+ const S_KEY_RED = s("red", true);
21
+ const S_KEY_CYAN = s("cyan", true);
22
22
  // Theme-dependent styles — lazily initialized on first rasterize() call
23
23
  let S_USER;
24
24
  let S_ASSISTANT;
@@ -41,7 +41,7 @@ function ensureStyles() {
41
41
  S_YELLOW = s(t.tool);
42
42
  S_GREEN = s(t.success);
43
43
  }
44
- const SPINNER_CHARS = ['', '', '', '', '', '', '', '', '', ''];
44
+ const SPINNER_CHARS = ["", "", "", "", "", "", "", "", "", ""];
45
45
  // ── Shared rendering helpers ──
46
46
  // Each takes (state, grid, row, limit, ...options) and returns next row.
47
47
  function renderBannerSection(state, grid, r, limit, opts) {
@@ -65,12 +65,12 @@ function renderThinkingSection(state, grid, r, limit) {
65
65
  return r;
66
66
  const w = grid.width;
67
67
  if (state.thinkingExpanded) {
68
- const thinkLines = state.thinkingText.split('\n').slice(-10);
68
+ const thinkLines = state.thinkingText.split("\n").slice(-10);
69
69
  const shimmerPos = state.spinnerFrame % 20;
70
70
  for (const tLine of thinkLines) {
71
71
  if (r >= limit)
72
72
  break;
73
- grid.writeText(r, 0, '💭 ', S_DIM);
73
+ grid.writeText(r, 0, "💭 ", S_DIM);
74
74
  const chars = [...tLine];
75
75
  for (let ci = 0; ci < chars.length && ci + 3 < w; ci++) {
76
76
  grid.setCell(r, 3 + ci, chars[ci], Math.abs(ci - shimmerPos) <= 2 ? S_BRIGHT : S_DIM);
@@ -79,9 +79,9 @@ function renderThinkingSection(state, grid, r, limit) {
79
79
  }
80
80
  }
81
81
  else {
82
- const lineCount = state.thinkingText.split('\n').length;
82
+ const lineCount = state.thinkingText.split("\n").length;
83
83
  const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
84
- const summary = `∴ Thinking${elapsed > 0 ? ` (${elapsed}s)` : ''} — ${lineCount} lines [Ctrl+O expand]`;
84
+ const summary = `∴ Thinking${elapsed > 0 ? ` (${elapsed}s)` : ""} — ${lineCount} lines [Ctrl+O expand]`;
85
85
  grid.writeText(r, 0, summary, S_DIM);
86
86
  r++;
87
87
  }
@@ -96,27 +96,27 @@ function renderThinkingSummarySection(state, grid, r, limit) {
96
96
  function renderSpinnerSection(state, grid, r, limit) {
97
97
  if (!state.loading || state.streamingText || state.thinkingText || r >= limit)
98
98
  return r;
99
- const w = grid.width;
100
- const thinkText = 'Thinking';
99
+ const _w = grid.width;
100
+ const thinkText = "Thinking";
101
101
  const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
102
102
  const t = getTheme();
103
103
  const baseColor = elapsed > 60 ? t.error : elapsed > 30 ? t.stall : t.primary;
104
104
  const shimmerColor = elapsed > 60 ? t.stallShimmer : elapsed > 30 ? t.warning : t.primaryShimmer;
105
105
  const baseStyle = { fg: baseColor, bg: null, bold: false, dim: false, underline: false };
106
- grid.writeText(r, 0, '', { ...baseStyle, bold: true });
106
+ grid.writeText(r, 0, "", { ...baseStyle, bold: true });
107
107
  const shimmerPos = state.spinnerFrame % (thinkText.length + 6);
108
108
  const shimmerStyle = { fg: shimmerColor, bg: null, bold: true, dim: false, underline: false };
109
109
  for (let ci = 0; ci < thinkText.length; ci++) {
110
110
  grid.setCell(r, 2 + ci, thinkText[ci], Math.abs(ci - shimmerPos) <= 1 ? shimmerStyle : baseStyle);
111
111
  }
112
- let suffix = '';
112
+ let suffix = "";
113
113
  if (elapsed > 0)
114
114
  suffix += ` ${elapsed}s`;
115
115
  if (state.tokenCount > 0) {
116
116
  const tokStr = state.tokenCount >= 1000 ? `${(state.tokenCount / 1000).toFixed(1)}K` : `${state.tokenCount}`;
117
117
  suffix += ` | ${tokStr} tokens`;
118
118
  }
119
- suffix += '...';
119
+ suffix += "...";
120
120
  grid.writeText(r, 2 + thinkText.length, suffix, S_DIM);
121
121
  return r + 1;
122
122
  }
@@ -124,7 +124,7 @@ function renderErrorSection(state, grid, r, limit) {
124
124
  if (!state.errorText || r >= limit)
125
125
  return r;
126
126
  const w = grid.width;
127
- grid.writeText(r, 0, '', S_ERROR);
127
+ grid.writeText(r, 0, "", S_ERROR);
128
128
  grid.writeText(r, 2, state.errorText.slice(0, w - 4), S_ERROR);
129
129
  return r + 1;
130
130
  }
@@ -133,17 +133,25 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
133
133
  for (const [callId, tc] of state.toolCalls) {
134
134
  if (r >= limit)
135
135
  break;
136
- const isAgent = tc.isAgent || tc.toolName === 'Agent' || tc.toolName === 'ParallelAgents';
136
+ const isAgent = tc.isAgent || tc.toolName === "Agent" || tc.toolName === "ParallelAgents";
137
137
  const icon = isAgent
138
- ? (tc.status === 'running' ? '⊕' : tc.status === 'done' ? '◈' : '◇')
139
- : (tc.status === 'running' ? SPINNER_CHARS[state.spinnerFrame % SPINNER_CHARS.length] : tc.status === 'done' ? '✓' : '✗');
140
- const statusStyle = tc.status === 'error' ? S_ERROR : tc.status === 'done' ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
138
+ ? tc.status === "running"
139
+ ? "⊕"
140
+ : tc.status === "done"
141
+ ? "◈"
142
+ : "◇"
143
+ : tc.status === "running"
144
+ ? SPINNER_CHARS[state.spinnerFrame % SPINNER_CHARS.length]
145
+ : tc.status === "done"
146
+ ? "✓"
147
+ : "✗";
148
+ const statusStyle = tc.status === "error" ? S_ERROR : tc.status === "done" ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
141
149
  const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
142
150
  const isExpanded = state.expandedToolCalls.has(callId);
143
- const canExpand = tc.status !== 'running' && tc.output;
151
+ const canExpand = tc.status !== "running" && tc.output;
144
152
  // Collapse/expand indicator
145
153
  if (canExpand) {
146
- grid.writeText(r, 0, isExpanded ? '' : '', S_DIM);
154
+ grid.writeText(r, 0, isExpanded ? "" : "", S_DIM);
147
155
  }
148
156
  grid.writeText(r, 2, `${icon} `, statusStyle);
149
157
  grid.writeText(r, 4, tc.toolName, nameStyle);
@@ -151,13 +159,13 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
151
159
  if (tc.args) {
152
160
  const maxArgs = w - afterName - 15;
153
161
  if (maxArgs > 5) {
154
- const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? '' : '');
162
+ const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? "" : "");
155
163
  grid.writeText(r, afterName, argsText, S_DIM);
156
164
  afterName += argsText.length + 1;
157
165
  }
158
166
  }
159
167
  // Elapsed time for running tools
160
- if (tc.status === 'running' && tc.startedAt) {
168
+ if (tc.status === "running" && tc.startedAt) {
161
169
  const elapsed = Math.floor((Date.now() - tc.startedAt) / 1000);
162
170
  if (elapsed > 0) {
163
171
  const lineCount = tc.liveOutput?.length ?? 0;
@@ -166,7 +174,7 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
166
174
  }
167
175
  }
168
176
  // Result summary for completed tools
169
- if (tc.status !== 'running' && tc.resultSummary) {
177
+ if (tc.status !== "running" && tc.resultSummary) {
170
178
  const elapsed = tc.startedAt ? Math.floor((Date.now() - tc.startedAt) / 1000) : 0;
171
179
  const suffix = elapsed > 0 ? `${tc.resultSummary} · ${elapsed}s` : tc.resultSummary;
172
180
  grid.writeText(r, Math.min(afterName, w - suffix.length - 2), suffix, S_DIM);
@@ -178,7 +186,7 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
178
186
  r++;
179
187
  }
180
188
  // Live streaming output while running
181
- if (tc.status === 'running' && tc.liveOutput && tc.liveOutput.length > 0) {
189
+ if (tc.status === "running" && tc.liveOutput && tc.liveOutput.length > 0) {
182
190
  const overflow = tc.liveOutput.length > opts.maxLiveLines ? tc.liveOutput.length - opts.maxLiveLines : 0;
183
191
  if (opts.showOverflow && overflow > 0 && r < limit) {
184
192
  grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
@@ -193,7 +201,7 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
193
201
  }
194
202
  }
195
203
  // Final output — collapsed by default (only show when expanded via Tab)
196
- if (tc.output && tc.status !== 'running' && isExpanded && r < limit) {
204
+ if (tc.output && tc.status !== "running" && isExpanded && r < limit) {
197
205
  // Image results: show inline placeholder
198
206
  if (isImageOutput(tc.output)) {
199
207
  const label = renderImageInline(tc.output);
@@ -201,13 +209,13 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
201
209
  r++;
202
210
  continue;
203
211
  }
204
- const outLines = tc.output.split('\n');
212
+ const outLines = tc.output.split("\n");
205
213
  const maxOut = 20;
206
214
  const showLines = outLines.slice(0, maxOut);
207
215
  for (const line of showLines) {
208
216
  if (r >= limit)
209
217
  break;
210
- const lineStyle = tc.status === 'error' ? S_ERROR : S_DIM;
218
+ const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
211
219
  grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
212
220
  r++;
213
221
  }
@@ -222,7 +230,13 @@ function renderToolCallsSection(state, grid, r, limit, opts) {
222
230
  function renderContextWarningSection(state, grid, r, limit) {
223
231
  if (!state.contextWarning || r >= limit)
224
232
  return r;
225
- const warnStyle = { fg: 'yellow', bg: null, bold: state.contextWarning.critical, dim: false, underline: false };
233
+ const warnStyle = {
234
+ fg: "yellow",
235
+ bg: null,
236
+ bold: state.contextWarning.critical,
237
+ dim: false,
238
+ underline: false,
239
+ };
226
240
  grid.writeText(r, 0, state.contextWarning.text, warnStyle);
227
241
  return r + 1;
228
242
  }
@@ -231,84 +245,84 @@ function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
231
245
  return nextRow;
232
246
  const w = grid.width;
233
247
  const { toolName, description, riskLevel } = state.permissionBox;
234
- const riskColor = riskLevel === 'high' ? 'red' : riskLevel === 'medium' ? 'yellow' : 'green';
248
+ const riskColor = riskLevel === "high" ? "red" : riskLevel === "medium" ? "yellow" : "green";
235
249
  const riskStyle = { fg: riskColor, bg: null, bold: true, dim: false, underline: false };
236
250
  if (opts.boxed) {
237
- if ((h - nextRow) < 6)
251
+ if (h - nextRow < 6)
238
252
  return nextRow;
239
253
  const riskDim = { fg: riskColor, bg: null, bold: false, dim: true, underline: false };
240
254
  const boxWidth = Math.max(15, Math.min(w - 2, 70));
241
255
  // Top border
242
- grid.writeText(nextRow, 1, '╭' + ''.repeat(boxWidth - 2) + '╮', riskDim);
256
+ grid.writeText(nextRow, 1, `╭${"".repeat(boxWidth - 2)}╮`, riskDim);
243
257
  nextRow++;
244
258
  // Tool name + risk
245
- grid.writeText(nextRow, 1, '', riskDim);
246
- grid.writeText(nextRow, 3, '', riskStyle);
259
+ grid.writeText(nextRow, 1, "", riskDim);
260
+ grid.writeText(nextRow, 3, "", riskStyle);
247
261
  grid.writeText(nextRow, 5, toolName, { ...riskStyle });
248
262
  grid.writeText(nextRow, 5 + toolName.length, ` ${riskLevel} risk`, S_DIM);
249
- grid.writeText(nextRow, boxWidth, '', riskDim);
263
+ grid.writeText(nextRow, boxWidth, "", riskDim);
250
264
  nextRow++;
251
265
  // Description (truncated)
252
266
  const rawDesc = state.permissionBox.suggestion || description.slice(0, boxWidth - 6);
253
- const descText = rawDesc.replace(/\|/g, ' ').replace(/\\/g, '/');
254
- grid.writeText(nextRow, 1, '', riskDim);
267
+ const descText = rawDesc.replace(/\|/g, " ").replace(/\\/g, "/");
268
+ grid.writeText(nextRow, 1, "", riskDim);
255
269
  grid.writeText(nextRow, 3, descText.slice(0, boxWidth - 4), S_DIM);
256
- grid.writeText(nextRow, boxWidth, '', riskDim);
270
+ grid.writeText(nextRow, boxWidth, "", riskDim);
257
271
  nextRow++;
258
272
  // Inline diff
259
273
  if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
260
- grid.writeText(nextRow, 1, '', riskDim);
274
+ grid.writeText(nextRow, 1, "", riskDim);
261
275
  nextRow++;
262
276
  const availDiffRows = Math.min(opts.maxDiffHeight, h - nextRow - 3);
263
277
  const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, boxWidth - 2, availDiffRows);
264
278
  for (let dr = 0; dr < diffRows; dr++) {
265
279
  if (nextRow + dr < grid.height) {
266
- grid.setCell(nextRow + dr, 1, '', riskDim);
267
- grid.setCell(nextRow + dr, boxWidth, '', riskDim);
280
+ grid.setCell(nextRow + dr, 1, "", riskDim);
281
+ grid.setCell(nextRow + dr, boxWidth, "", riskDim);
268
282
  }
269
283
  }
270
284
  nextRow += diffRows;
271
285
  }
272
286
  // Action keys
273
- grid.writeText(nextRow, 1, '', riskDim);
287
+ grid.writeText(nextRow, 1, "", riskDim);
274
288
  let kc = 3;
275
- grid.writeText(nextRow, kc, 'Y', S_KEY_GREEN);
289
+ grid.writeText(nextRow, kc, "Y", S_KEY_GREEN);
276
290
  kc += 1;
277
- grid.writeText(nextRow, kc, 'es', S_DIM);
291
+ grid.writeText(nextRow, kc, "es", S_DIM);
278
292
  kc += 2;
279
- grid.writeText(nextRow, kc, ' ', S_DIM);
293
+ grid.writeText(nextRow, kc, " ", S_DIM);
280
294
  kc += 2;
281
- grid.writeText(nextRow, kc, 'N', S_KEY_RED);
295
+ grid.writeText(nextRow, kc, "N", S_KEY_RED);
282
296
  kc += 1;
283
- grid.writeText(nextRow, kc, 'o', S_DIM);
297
+ grid.writeText(nextRow, kc, "o", S_DIM);
284
298
  kc += 1;
285
299
  if (state.permissionDiffInfo) {
286
- grid.writeText(nextRow, kc, ' ', S_DIM);
300
+ grid.writeText(nextRow, kc, " ", S_DIM);
287
301
  kc += 2;
288
- grid.writeText(nextRow, kc, 'D', S_KEY_CYAN);
302
+ grid.writeText(nextRow, kc, "D", S_KEY_CYAN);
289
303
  kc += 1;
290
- grid.writeText(nextRow, kc, 'iff', S_DIM);
304
+ grid.writeText(nextRow, kc, "iff", S_DIM);
291
305
  kc += 3;
292
306
  }
293
- grid.writeText(nextRow, boxWidth, '', riskDim);
307
+ grid.writeText(nextRow, boxWidth, "", riskDim);
294
308
  nextRow++;
295
309
  // Bottom border
296
- grid.writeText(nextRow, 1, '╰' + ''.repeat(boxWidth - 2) + '╯', riskDim);
310
+ grid.writeText(nextRow, 1, `╰${"".repeat(boxWidth - 2)}╯`, riskDim);
297
311
  nextRow++;
298
312
  }
299
313
  else {
300
314
  // Compact mode (rasterizeLive)
301
- if ((h - nextRow) < 4)
315
+ if (h - nextRow < 4)
302
316
  return nextRow;
303
317
  grid.writeText(nextRow, 1, `⚠ ${toolName} (${riskLevel} risk)`, riskStyle);
304
318
  nextRow++;
305
- grid.writeText(nextRow, 1, 'Y', S_KEY_GREEN);
306
- grid.writeText(nextRow, 2, 'es ', S_DIM);
307
- grid.writeText(nextRow, 6, 'N', S_KEY_RED);
308
- grid.writeText(nextRow, 7, 'o', S_DIM);
319
+ grid.writeText(nextRow, 1, "Y", S_KEY_GREEN);
320
+ grid.writeText(nextRow, 2, "es ", S_DIM);
321
+ grid.writeText(nextRow, 6, "N", S_KEY_RED);
322
+ grid.writeText(nextRow, 7, "o", S_DIM);
309
323
  if (state.permissionDiffInfo) {
310
- grid.writeText(nextRow, 10, 'D', S_KEY_CYAN);
311
- grid.writeText(nextRow, 11, 'iff', S_DIM);
324
+ grid.writeText(nextRow, 10, "D", S_KEY_CYAN);
325
+ grid.writeText(nextRow, 11, "iff", S_DIM);
312
326
  }
313
327
  nextRow++;
314
328
  // Inline diff (when toggled)
@@ -325,37 +339,37 @@ function renderQuestionPromptSection(state, grid, nextRow, h, opts) {
325
339
  return { nextRow, questionInputRow: -1 };
326
340
  const w = grid.width;
327
341
  const { question, options, input, cursor } = state.questionPrompt;
328
- const qStyle = { fg: 'yellow', bg: null, bold: false, dim: false, underline: false };
342
+ const qStyle = { fg: "yellow", bg: null, bold: false, dim: false, underline: false };
329
343
  if (opts.boxed) {
330
- const qBorder = { fg: 'yellow', bg: null, bold: false, dim: true, underline: false };
344
+ const qBorder = { fg: "yellow", bg: null, bold: false, dim: true, underline: false };
331
345
  const qBoxWidth = Math.max(15, Math.min(w - 2, 70));
332
- grid.writeText(nextRow, 1, '╭' + ''.repeat(qBoxWidth - 2) + '╮', qBorder);
346
+ grid.writeText(nextRow, 1, `╭${"".repeat(qBoxWidth - 2)}╮`, qBorder);
333
347
  nextRow++;
334
- grid.writeText(nextRow, 1, '', qBorder);
348
+ grid.writeText(nextRow, 1, "", qBorder);
335
349
  grid.writeText(nextRow, 3, `❓ ${question}`, qStyle);
336
- grid.writeText(nextRow, qBoxWidth, '', qBorder);
350
+ grid.writeText(nextRow, qBoxWidth, "", qBorder);
337
351
  nextRow++;
338
352
  if (options && options.length > 0) {
339
353
  for (let oi = 0; oi < options.length; oi++) {
340
- grid.writeText(nextRow, 1, '', qBorder);
354
+ grid.writeText(nextRow, 1, "", qBorder);
341
355
  grid.writeText(nextRow, 5, `${oi + 1}. ${options[oi]}`, S_DIM);
342
- grid.writeText(nextRow, qBoxWidth, '', qBorder);
356
+ grid.writeText(nextRow, qBoxWidth, "", qBorder);
343
357
  nextRow++;
344
358
  }
345
359
  }
346
360
  const questionInputRow = nextRow;
347
- grid.writeText(nextRow, 1, '', qBorder);
348
- grid.writeText(nextRow, 3, '', qStyle);
361
+ grid.writeText(nextRow, 1, "", qBorder);
362
+ grid.writeText(nextRow, 3, "", qStyle);
349
363
  grid.writeText(nextRow, 5, input, S_TEXT);
350
- grid.writeText(nextRow, qBoxWidth, '', qBorder);
364
+ grid.writeText(nextRow, qBoxWidth, "", qBorder);
351
365
  nextRow++;
352
- grid.writeText(nextRow, 1, '╰' + ''.repeat(qBoxWidth - 2) + '╯', qBorder);
366
+ grid.writeText(nextRow, 1, `╰${"".repeat(qBoxWidth - 2)}╯`, qBorder);
353
367
  nextRow++;
354
368
  return { nextRow, questionInputRow };
355
369
  }
356
370
  else {
357
371
  // Compact mode (rasterizeLive)
358
- if ((h - nextRow) < 3)
372
+ if (h - nextRow < 3)
359
373
  return { nextRow, questionInputRow: -1 };
360
374
  grid.writeText(nextRow, 1, `❓ ${question}`, S_TEXT);
361
375
  nextRow++;
@@ -368,7 +382,7 @@ function renderQuestionPromptSection(state, grid, nextRow, h, opts) {
368
382
  }
369
383
  }
370
384
  const questionInputRow = nextRow;
371
- grid.writeText(nextRow, 1, '', S_USER);
385
+ grid.writeText(nextRow, 1, "", S_USER);
372
386
  grid.writeText(nextRow, 3, input, S_TEXT);
373
387
  nextRow++;
374
388
  return { nextRow, questionInputRow };
@@ -388,7 +402,7 @@ function renderAutocompleteSection(state, grid, nextRow, limit, promptWidth) {
388
402
  if (nextRow >= limit)
389
403
  break;
390
404
  const cmd = state.autocomplete[ai];
391
- const desc = state.autocompleteDescriptions[ai] ?? '';
405
+ const desc = state.autocompleteDescriptions[ai] ?? "";
392
406
  const selected = ai === state.autocompleteIndex;
393
407
  const acStyle = selected ? s(getTheme().user, true) : s(null, false, true);
394
408
  grid.writeText(nextRow, promptWidth, `/${cmd.padEnd(12)}`, acStyle);
@@ -412,7 +426,7 @@ function renderNotificationsSection(state, grid, nextRow, limit) {
412
426
  function renderInputSection(state, grid, inputRow, limit, promptText, promptWidth) {
413
427
  grid.writeText(inputRow, 0, promptText, S_USER);
414
428
  const inputStart = promptWidth;
415
- const inputLines = state.inputText.split('\n');
429
+ const inputLines = state.inputText.split("\n");
416
430
  const maxInputLines = Math.min(inputLines.length, 5);
417
431
  for (let li = 0; li < maxInputLines; li++) {
418
432
  if (inputRow + li >= limit)
@@ -433,9 +447,7 @@ function renderInputSection(state, grid, inputRow, limit, promptText, promptWidt
433
447
  }
434
448
  const hintsRow = inputRow + maxInputLines;
435
449
  if (hintsRow < limit) {
436
- const hintsText = inputLines.length > 1
437
- ? `${state.statusHints} | Alt+Enter newline`
438
- : state.statusHints;
450
+ const hintsText = inputLines.length > 1 ? `${state.statusHints} | Alt+Enter newline` : state.statusHints;
439
451
  grid.writeText(hintsRow, 0, hintsText, S_DIM);
440
452
  }
441
453
  return inputRow + maxInputLines + 1;
@@ -444,11 +456,11 @@ function renderCompanionSection(state, grid, anchorRow, limit, promptWidth) {
444
456
  if (!state.companionLines || grid.width < 50)
445
457
  return;
446
458
  const w = grid.width;
447
- const compWidth = Math.max(...state.companionLines.map(l => l.length), 0);
459
+ const compWidth = Math.max(...state.companionLines.map((l) => l.length), 0);
448
460
  const compStartCol = Math.max(0, w - compWidth - 1);
449
461
  if (compStartCol <= promptWidth + 20)
450
462
  return;
451
- const compStyle = { fg: state.companionColor || 'cyan', bg: null, bold: false, dim: false, underline: false };
463
+ const compStyle = { fg: state.companionColor || "cyan", bg: null, bold: false, dim: false, underline: false };
452
464
  for (let i = 0; i < state.companionLines.length; i++) {
453
465
  const compRow = anchorRow + i;
454
466
  if (compRow >= limit)
@@ -462,14 +474,14 @@ function computeCursorPosition(state, inputRow, inputStart, questionInputRow) {
462
474
  return { cursorRow: questionInputRow, cursorCol: 5 + state.questionPrompt.cursor };
463
475
  }
464
476
  const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
465
- const cursorLines = textBeforeCursor.split('\n');
477
+ const cursorLines = textBeforeCursor.split("\n");
466
478
  const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
467
479
  const cursorColInLine = cursorLines[cursorLines.length - 1].length;
468
480
  return { cursorRow: inputRow + cursorLineIdx, cursorCol: inputStart + cursorColInLine };
469
481
  }
470
482
  function getPromptText(state) {
471
- const vimIndicator = state.vimMode ? (state.vimMode === 'normal' ? '[N] ' : '[I] ') : '';
472
- const promptText = vimIndicator + '';
483
+ const vimIndicator = state.vimMode ? (state.vimMode === "normal" ? "[N] " : "[I] ") : "";
484
+ const promptText = `${vimIndicator}`;
473
485
  return { promptText, promptWidth: promptText.length };
474
486
  }
475
487
  // ── Main rasterization functions ──
@@ -485,14 +497,17 @@ export function rasterize(state, grid) {
485
497
  // Footer height — capped at 50% of terminal to preserve message area
486
498
  const companionHeight = state.companionLines ? Math.min(state.companionLines.length + 1, 8) : 0;
487
499
  const maxDiffHeight = Math.min(15, Math.floor(h / 3));
488
- const diffHeight = (state.permissionDiffVisible && state.permissionDiffInfo) ? maxDiffHeight : 0;
500
+ const diffHeight = state.permissionDiffVisible && state.permissionDiffInfo ? maxDiffHeight : 0;
489
501
  const permissionHeight = state.permissionBox ? 6 + diffHeight : 0;
490
502
  const questionHeight = state.questionPrompt ? 4 + (state.questionPrompt.options?.length ?? 0) : 0;
491
503
  const statusLineHeight = state.statusLine ? 1 : 0;
492
504
  const contextWarningHeight = state.contextWarning ? 1 : 0;
493
505
  const autocompleteHeight = state.autocomplete.length;
494
506
  const inputLineCount = Math.min(5, (state.inputText.match(/\n/g)?.length ?? 0) + 1);
495
- const rawFooterHeight = Math.max(2 + inputLineCount + statusLineHeight + autocompleteHeight, companionHeight + 1) + permissionHeight + questionHeight + contextWarningHeight;
507
+ const rawFooterHeight = Math.max(2 + inputLineCount + statusLineHeight + autocompleteHeight, companionHeight + 1) +
508
+ permissionHeight +
509
+ questionHeight +
510
+ contextWarningHeight;
496
511
  const footerHeight = Math.min(rawFooterHeight, Math.floor(h / 2));
497
512
  const msgAreaHeight = Math.max(1, h - footerHeight);
498
513
  // ── Session browser overlay ──
@@ -500,30 +515,48 @@ export function rasterize(state, grid) {
500
515
  const browserRows = renderSessionBrowser(grid, 0, 0, state.sessionBrowser, w, msgAreaHeight);
501
516
  const footerStart = Math.min(browserRows, msgAreaHeight);
502
517
  for (let c = 0; c < w; c++)
503
- grid.setCell(footerStart, c, '', S_BORDER);
518
+ grid.setCell(footerStart, c, "", S_BORDER);
504
519
  const inputRow = footerStart + 1;
505
- grid.writeText(inputRow, 0, '', S_USER);
506
- grid.writeText(inputRow + 1, 0, '↑/↓ navigate | Enter resume | Esc cancel', S_DIM);
520
+ grid.writeText(inputRow, 0, "", S_USER);
521
+ grid.writeText(inputRow + 1, 0, "↑/↓ navigate | Enter resume | Esc cancel", S_DIM);
507
522
  return { cursorRow: inputRow, cursorCol: 2 };
508
523
  }
509
524
  // ── Messages area (top) ──
510
525
  const allContent = [];
511
526
  for (const msg of state.messages) {
512
- if (msg.role === 'user') {
513
- allContent.push({ role: 'user', content: msg.content, style: { ...S_TEXT, bold: true }, prefixStyle: S_USER, prefix: '❯ ' });
514
- }
515
- else if (msg.role === 'assistant') {
516
- allContent.push({ role: 'assistant', content: msg.content, style: S_TEXT, prefixStyle: S_ASSISTANT, prefix: '◆ ' });
517
- }
518
- else if (msg.role === 'system') {
519
- allContent.push({ role: 'system', content: msg.content, style: S_DIM, prefixStyle: S_DIM, prefix: ' ' });
527
+ if (msg.role === "user") {
528
+ allContent.push({
529
+ role: "user",
530
+ content: msg.content,
531
+ style: { ...S_TEXT, bold: true },
532
+ prefixStyle: S_USER,
533
+ prefix: "❯ ",
534
+ });
535
+ }
536
+ else if (msg.role === "assistant") {
537
+ allContent.push({
538
+ role: "assistant",
539
+ content: msg.content,
540
+ style: S_TEXT,
541
+ prefixStyle: S_ASSISTANT,
542
+ prefix: "◆ ",
543
+ });
544
+ }
545
+ else if (msg.role === "system") {
546
+ allContent.push({ role: "system", content: msg.content, style: S_DIM, prefixStyle: S_DIM, prefix: " " });
520
547
  }
521
548
  }
522
549
  if (state.loading && state.streamingText) {
523
- allContent.push({ role: 'streaming', content: state.streamingText, style: S_TEXT, prefixStyle: S_ASSISTANT, prefix: '◆ ' });
550
+ allContent.push({
551
+ role: "streaming",
552
+ content: state.streamingText,
553
+ style: S_TEXT,
554
+ prefixStyle: S_ASSISTANT,
555
+ prefix: "◆ ",
556
+ });
524
557
  }
525
558
  if (state.errorText) {
526
- allContent.push({ role: 'error', content: state.errorText, style: S_ERROR, prefixStyle: S_ERROR, prefix: '' });
559
+ allContent.push({ role: "error", content: state.errorText, style: S_ERROR, prefixStyle: S_ERROR, prefix: "" });
527
560
  }
528
561
  const prefixLen = 2;
529
562
  const contentWidth = w - 1; // reserve rightmost column for scrollbar
@@ -536,20 +569,20 @@ export function rasterize(state, grid) {
536
569
  totalRows += visibleLines + 1;
537
570
  }
538
571
  for (const item of allContent) {
539
- if (item.role === 'user' && totalRows > 0)
572
+ if (item.role === "user" && totalRows > 0)
540
573
  totalRows++;
541
- if (item.role === 'assistant' || item.role === 'streaming') {
574
+ if (item.role === "assistant" || item.role === "streaming") {
542
575
  totalRows += measureMarkdown(item.content, contentWidth);
543
576
  }
544
577
  else {
545
- const lines = item.content.split('\n');
578
+ const lines = item.content.split("\n");
546
579
  for (const line of lines) {
547
580
  totalRows += Math.max(1, Math.ceil((line.length || 1) / textWidth));
548
581
  }
549
582
  }
550
583
  }
551
584
  if (state.thinkingText) {
552
- totalRows += state.thinkingExpanded ? Math.min(state.thinkingText.split('\n').length, 10) : 1;
585
+ totalRows += state.thinkingExpanded ? Math.min(state.thinkingText.split("\n").length, 10) : 1;
553
586
  }
554
587
  if (!state.loading && state.lastThinkingSummary)
555
588
  totalRows += 1;
@@ -559,10 +592,10 @@ export function rasterize(state, grid) {
559
592
  totalRows += 1;
560
593
  if (tc.isAgent && tc.agentDescription)
561
594
  totalRows += 1;
562
- if (tc.status === 'running' && tc.liveOutput)
595
+ if (tc.status === "running" && tc.liveOutput)
563
596
  totalRows += Math.min(tc.liveOutput.length, 5);
564
- if (tc.output && tc.status !== 'running' && state.expandedToolCalls.has(callId)) {
565
- totalRows += Math.min(tc.output.split('\n').length, 20);
597
+ if (tc.output && tc.status !== "running" && state.expandedToolCalls.has(callId)) {
598
+ totalRows += Math.min(tc.output.split("\n").length, 20);
566
599
  }
567
600
  }
568
601
  if (state.contextWarning)
@@ -602,21 +635,21 @@ export function rasterize(state, grid) {
602
635
  for (const item of allContent) {
603
636
  if (r >= msgAreaHeight)
604
637
  break;
605
- if (item.role === 'user' && contentIdx > 0) {
638
+ if (item.role === "user" && contentIdx > 0) {
606
639
  if (virtualR >= scrollOffset) {
607
640
  for (let c = 0; c < w; c++) {
608
- grid.setCell(r, c, '', S_BORDER);
641
+ grid.setCell(r, c, "", S_BORDER);
609
642
  }
610
643
  r++;
611
644
  }
612
645
  virtualR++;
613
646
  }
614
647
  let itemRows;
615
- if (item.role === 'assistant' || item.role === 'streaming') {
648
+ if (item.role === "assistant" || item.role === "streaming") {
616
649
  itemRows = measureMarkdown(item.content, contentWidth);
617
650
  }
618
651
  else {
619
- const lines = item.content.split('\n');
652
+ const lines = item.content.split("\n");
620
653
  itemRows = 0;
621
654
  for (const line of lines) {
622
655
  itemRows += Math.max(1, Math.ceil((line.length || 1) / textWidth));
@@ -629,7 +662,7 @@ export function rasterize(state, grid) {
629
662
  }
630
663
  grid.writeText(r, 0, item.prefix, item.prefixStyle);
631
664
  let rows;
632
- if (item.role === 'assistant' || item.role === 'streaming') {
665
+ if (item.role === "assistant" || item.role === "streaming") {
633
666
  rows = renderMarkdown(grid, r, prefixLen, item.content, contentWidth, state.codeBlocksExpanded, msgAreaHeight);
634
667
  }
635
668
  else {
@@ -651,16 +684,16 @@ export function rasterize(state, grid) {
651
684
  const S_THUMB = { fg: null, bg: null, bold: false, dim: false, underline: false };
652
685
  for (let sr = 0; sr < msgAreaHeight; sr++) {
653
686
  const isThumb = sr >= thumbStart && sr < thumbStart + thumbSize;
654
- grid.setCell(sr, w - 1, isThumb ? '' : '', isThumb ? S_THUMB : S_TRACK);
687
+ grid.setCell(sr, w - 1, isThumb ? "" : "", isThumb ? S_THUMB : S_TRACK);
655
688
  }
656
689
  }
657
690
  // ── Footer ──
658
691
  const footerStart = Math.min(r, msgAreaHeight);
659
692
  for (let c = 0; c < w; c++) {
660
- grid.setCell(footerStart, c, '', S_BORDER);
693
+ grid.setCell(footerStart, c, "", S_BORDER);
661
694
  }
662
695
  if (hasScrollbar) {
663
- grid.setCell(footerStart, w - 1, '', S_BORDER);
696
+ grid.setCell(footerStart, w - 1, "", S_BORDER);
664
697
  }
665
698
  if (state.manualScroll > 0 && totalRows > msgAreaHeight) {
666
699
  const hiddenBelow = state.manualScroll;
@@ -687,11 +720,17 @@ export function rasterize(state, grid) {
687
720
  renderInputSection(state, grid, inputRow, h, promptText, promptWidth);
688
721
  // Companion (right-aligned in footer, skipped if it would overlap input)
689
722
  if (state.companionLines && w >= 50) {
690
- const compWidth = Math.max(...state.companionLines.map(l => l.length), 0);
723
+ const compWidth = Math.max(...state.companionLines.map((l) => l.length), 0);
691
724
  const compStartCol = Math.max(0, w - compWidth - 1);
692
- const inputEndCol = promptWidth + (state.inputText.split('\n')[0]?.length ?? 0);
725
+ const inputEndCol = promptWidth + (state.inputText.split("\n")[0]?.length ?? 0);
693
726
  if (compStartCol > inputEndCol + 3) {
694
- const compStyle = { fg: state.companionColor || 'cyan', bg: null, bold: false, dim: false, underline: false };
727
+ const compStyle = {
728
+ fg: state.companionColor || "cyan",
729
+ bg: null,
730
+ bold: false,
731
+ dim: false,
732
+ underline: false,
733
+ };
695
734
  for (let i = 0; i < state.companionLines.length; i++) {
696
735
  const compRow = footerStart + i;
697
736
  if (compRow >= inputRow)
@@ -721,7 +760,7 @@ export function rasterizeLive(state, grid) {
721
760
  }
722
761
  // ── Streaming text ──
723
762
  if (state.loading && state.streamingText) {
724
- grid.writeText(r, 0, '', S_ASSISTANT);
763
+ grid.writeText(r, 0, "", S_ASSISTANT);
725
764
  const rows = renderMarkdown(grid, r, 2, state.streamingText, w, state.codeBlocksExpanded, h);
726
765
  r += rows;
727
766
  }
@@ -735,7 +774,7 @@ export function rasterizeLive(state, grid) {
735
774
  // ── Footer border ──
736
775
  if (r < h) {
737
776
  for (let c = 0; c < w; c++)
738
- grid.setCell(r, c, '', S_BORDER);
777
+ grid.setCell(r, c, "", S_BORDER);
739
778
  r++;
740
779
  }
741
780
  let nextRow = r;
@@ -758,7 +797,7 @@ export function rasterizeLive(state, grid) {
758
797
  return { cursorRow: questionInputRow, cursorCol: 3 + state.questionPrompt.cursor };
759
798
  }
760
799
  const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
761
- const cursorLines = textBeforeCursor.split('\n');
800
+ const cursorLines = textBeforeCursor.split("\n");
762
801
  const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
763
802
  const cursorColInLine = cursorLines[cursorLines.length - 1].length;
764
803
  return { cursorRow: inputRow + cursorLineIdx, cursorCol: promptWidth + cursorColInLine };