@zhijiewang/openharness 2.24.0 → 2.25.0

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.
@@ -4,6 +4,7 @@
4
4
  * right after the scrollback content each frame (no absolute positioning gap).
5
5
  */
6
6
  import { recordApproval } from "../harness/approvals.js";
7
+ import { appendToolPermission } from "../harness/config.js";
7
8
  import { getTheme } from "../utils/theme-data.js";
8
9
  import { summarizeToolArgs } from "../utils/tool-summary.js";
9
10
  import { CellGrid } from "./cells.js";
@@ -396,9 +397,6 @@ export class TerminalRenderer {
396
397
  // exists (we don't auto-create on first interaction).
397
398
  if (k === "a" && toolName) {
398
399
  try {
399
- // Lazy import to avoid pulling config into the renderer bundle
400
- // for callers that don't trip the permission path.
401
- const { appendToolPermission } = require("../harness/config.js");
402
400
  appendToolPermission(toolName);
403
401
  }
404
402
  catch {
@@ -5,6 +5,8 @@
5
5
  import { getTheme } from "../utils/theme-data.js";
6
6
  import { renderDiff } from "./diff.js";
7
7
  import { isImageOutput, renderImageInline } from "./image.js";
8
+ import { deriveSpinnerLabel } from "./spinner-label.js";
9
+ import { toolColor } from "./tool-color.js";
8
10
  // ── Style constants ──
9
11
  const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
10
12
  export const S_TEXT = s(null);
@@ -93,14 +95,14 @@ export function renderThinkingSummarySection(state, grid, r, limit) {
93
95
  export function renderSpinnerSection(state, grid, r, limit) {
94
96
  if (!state.loading || state.streamingText || state.thinkingText || r >= limit)
95
97
  return r;
96
- const thinkText = "Thinking";
98
+ const thinkText = deriveSpinnerLabel(state.toolCalls);
97
99
  const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
98
100
  const t = getTheme();
99
101
  const baseColor = elapsed > 60 ? t.error : elapsed > 30 ? t.stall : t.primary;
100
102
  const shimmerColor = elapsed > 60 ? t.stallShimmer : elapsed > 30 ? t.warning : t.primaryShimmer;
101
103
  const baseStyle = { fg: baseColor, bg: null, bold: false, dim: false, underline: false };
102
104
  grid.writeText(r, 0, "◆ ", { ...baseStyle, bold: true });
103
- const shimmerPos = state.spinnerFrame % (thinkText.length + 6);
105
+ const shimmerPos = state.spinnerFrame % 20;
104
106
  const shimmerStyle = { fg: shimmerColor, bg: null, bold: true, dim: false, underline: false };
105
107
  for (let ci = 0; ci < thinkText.length; ci++) {
106
108
  grid.setCell(r, 2 + ci, thinkText[ci], Math.abs(ci - shimmerPos) <= 1 ? shimmerStyle : baseStyle);
@@ -141,8 +143,9 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
141
143
  : tc.status === "done"
142
144
  ? "✓"
143
145
  : "✗";
144
- const statusStyle = tc.status === "error" ? S_ERROR : tc.status === "done" ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
145
- const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
146
+ const toolStyle = { fg: toolColor(tc.toolName), bg: null, bold: false, dim: false, underline: false };
147
+ const statusStyle = tc.status === "error" ? S_ERROR : tc.status === "done" ? S_GREEN : isAgent ? S_AGENT : toolStyle;
148
+ const nameStyle = isAgent ? S_AGENT : { ...toolStyle, bold: true };
146
149
  const isExpanded = state.expandedToolCalls.has(callId);
147
150
  const canExpand = tc.status !== "running" && tc.output;
148
151
  if (canExpand) {
@@ -427,21 +430,33 @@ export function renderInputSection(state, grid, inputRow, limit, promptText, pro
427
430
  const inputStart = promptWidth;
428
431
  const inputLines = state.inputText.split("\n");
429
432
  const maxInputLines = Math.min(inputLines.length, 5);
433
+ // Pre-compute the [N lines] suffix column on row 0 (if multi-line) so the
434
+ // wrap glyph for line 0 can yield to the suffix when they would collide.
435
+ // The +1 reserves the glyph column so the suffix starts one column after
436
+ // it (produces "text↵ [N lines]"); a +0 would let the suffix overwrite the
437
+ // glyph on narrow terminals.
438
+ let lineCountCol = -1;
439
+ if (inputLines.length > 1) {
440
+ const lineCountStr = ` [${inputLines.length} lines]`;
441
+ lineCountCol = Math.min(inputStart + (inputLines[0]?.length ?? 0) + 1, grid.width - lineCountStr.length - 1);
442
+ }
430
443
  for (let li = 0; li < maxInputLines; li++) {
431
444
  if (inputRow + li >= limit)
432
445
  break;
433
- if (li === 0) {
434
- grid.writeText(inputRow, inputStart, inputLines[0], S_TEXT);
435
- }
436
- else {
437
- grid.writeText(inputRow + li, inputStart, inputLines[li], S_TEXT);
446
+ const lineText = inputLines[li];
447
+ grid.writeText(inputRow + li, inputStart, lineText, S_TEXT);
448
+ // Audit U-C2: append a dim ↵ continuation glyph to every non-last line.
449
+ if (li < inputLines.length - 1) {
450
+ const glyphCol = inputStart + lineText.length;
451
+ const wouldCollideWithLineCount = li === 0 && lineCountCol > inputStart && glyphCol >= lineCountCol;
452
+ if (glyphCol < grid.width && !wouldCollideWithLineCount) {
453
+ grid.writeText(inputRow + li, glyphCol, "↵", S_DIM);
454
+ }
438
455
  }
439
456
  }
440
- if (inputLines.length > 1) {
457
+ if (inputLines.length > 1 && lineCountCol > inputStart) {
441
458
  const lineCountStr = ` [${inputLines.length} lines]`;
442
- const lineCountCol = Math.min(inputStart + (inputLines[0]?.length ?? 0) + 1, grid.width - lineCountStr.length - 1);
443
- if (lineCountCol > inputStart)
444
- grid.writeText(inputRow, lineCountCol, lineCountStr, S_DIM);
459
+ grid.writeText(inputRow, lineCountCol, lineCountStr, S_DIM);
445
460
  }
446
461
  const hintsRow = inputRow + maxInputLines;
447
462
  if (hintsRow < limit) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Derives the spinner section label from the live tool-call map.
3
+ */
4
+ import type { ToolCallInfo } from "./layout.js";
5
+ export declare function deriveSpinnerLabel(toolCalls: Map<string, ToolCallInfo>): string;
6
+ //# sourceMappingURL=spinner-label.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Derives the spinner section label from the live tool-call map.
3
+ */
4
+ export function deriveSpinnerLabel(toolCalls) {
5
+ const running = [];
6
+ for (const tc of toolCalls.values()) {
7
+ if (tc.status === "running")
8
+ running.push(tc);
9
+ }
10
+ if (running.length === 0)
11
+ return "Thinking";
12
+ if (running.length === 1) {
13
+ const name = running[0].toolName;
14
+ if (name.startsWith("mcp__")) {
15
+ const rest = name.slice("mcp__".length);
16
+ const idx = rest.indexOf("__");
17
+ if (idx > 0)
18
+ return `Calling ${rest.slice(0, idx)}:${rest.slice(idx + 2)}`;
19
+ }
20
+ return `Running ${name}`;
21
+ }
22
+ return `Running ${running.length} tools`;
23
+ }
24
+ //# sourceMappingURL=spinner-label.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Maps a tool name to a display color for the tool-call section.
3
+ * Read-class tools → cyan, Mutate-class → yellow, Exec-class → magenta,
4
+ * MCP tools (mcp__ prefix) → green, everything else → yellow fallback.
5
+ */
6
+ export type ToolColor = "cyan" | "yellow" | "magenta" | "green";
7
+ export declare function toolColor(toolName: string): ToolColor;
8
+ //# sourceMappingURL=tool-color.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Maps a tool name to a display color for the tool-call section.
3
+ * Read-class tools → cyan, Mutate-class → yellow, Exec-class → magenta,
4
+ * MCP tools (mcp__ prefix) → green, everything else → yellow fallback.
5
+ */
6
+ const READ = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch", "ExaSearch"]);
7
+ const MUTATE = new Set(["Edit", "Write", "NotebookEdit"]);
8
+ const EXEC = new Set(["Bash", "PowerShell"]);
9
+ export function toolColor(toolName) {
10
+ if (READ.has(toolName))
11
+ return "cyan";
12
+ if (MUTATE.has(toolName))
13
+ return "yellow";
14
+ if (EXEC.has(toolName))
15
+ return "magenta";
16
+ if (toolName.startsWith("mcp__"))
17
+ return "green";
18
+ return "yellow";
19
+ }
20
+ //# sourceMappingURL=tool-color.js.map
package/dist/repl.js CHANGED
@@ -977,7 +977,6 @@ export async function startREPL(config) {
977
977
  case "tool_call_end": {
978
978
  const toolName = callIdToToolName.get(event.callId) ?? event.callId;
979
979
  const prevTc = renderer.getToolCall(event.callId);
980
- const _elapsed = prevTc?.startedAt ? Math.floor((Date.now() - prevTc.startedAt) / 1000) : 0;
981
980
  renderer.setToolCall(event.callId, {
982
981
  toolName,
983
982
  status: event.isError ? "error" : "done",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {