nou-cli 0.4.0 → 0.5.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.
@@ -1,45 +1,81 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
2
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useAnimation, useInput } from "ink";
4
4
  import { renderMarkdown, wrap } from "./text.js";
5
5
  import { ChatInput, inputHeight } from "./ChatInput.js";
6
6
  import { C, SPINNER } from "./theme.js";
7
7
  import { Pane } from "./Pane.js";
8
+ import { Scrollbar, topFor } from "./scroll.js";
9
+ const BAR = "▌ ";
8
10
  export const COMMANDS = ["/model", "/models", "/settings", "/auto", "/engine", "/copy", "/undo", "/rewind", "/history", "/problems", "/version", "/clear", "/help"];
9
- export function Chat({ width, height, focused, entries, streaming, busy, approval, approvalNote, contextChip, wheel, onSubmit, onAnswer }) {
11
+ /** Display lines for one message, wrapped to `inner` columns. */
12
+ function entryLines(entry, inner) {
13
+ const out = [];
14
+ if (entry.kind === "user") {
15
+ out.push({ text: "you", color: C.cyan, bold: true, lead: BAR, leadColor: C.cyan });
16
+ for (const line of wrap(entry.text, inner - 3))
17
+ out.push({ text: line, color: C.text, lead: BAR, leadColor: C.cyan });
18
+ out.push({ text: "" });
19
+ }
20
+ else if (entry.kind === "assistant") {
21
+ out.push({ text: "◆ nou", color: C.accent2, bold: true, lead: BAR, leadColor: C.accent2 });
22
+ for (const line of renderMarkdown(entry.text, inner - 3)) {
23
+ const color = line.kind === "heading" ? C.accent2 : line.kind === "quote" ? C.dim : undefined;
24
+ out.push({ text: "", spans: line.spans, ...(color ? { color } : {}), lead: BAR, leadColor: C.accent2 });
25
+ }
26
+ out.push({ text: "" });
27
+ }
28
+ else {
29
+ const style = entry.kind === "tool"
30
+ ? { icon: "⚙", color: C.dim }
31
+ : entry.kind === "error" ? { icon: "✖", color: C.red } : { icon: "ℹ", color: C.accent };
32
+ wrap(entry.text, inner - 4).forEach((line, i) => out.push({ text: ` ${line}`, color: style.color, lead: i === 0 ? ` ${style.icon}` : " ", leadColor: style.color }));
33
+ }
34
+ return out;
35
+ }
36
+ // Finished messages never change, so each is laid out once per width; only the reply being streamed
37
+ // is redone as it grows. Without this every streamed token re-rendered the whole conversation.
38
+ const laidOut = new WeakMap();
39
+ function cachedLines(entry, inner) {
40
+ const hit = laidOut.get(entry);
41
+ if (hit?.inner === inner)
42
+ return hit.lines;
43
+ const lines = entryLines(entry, inner);
44
+ laidOut.set(entry, { inner, lines });
45
+ return lines;
46
+ }
47
+ export const Chat = memo(function Chat({ width, height, focused, entries, streaming, busy, approval, approvalNote, contextChip, wheel, scrollTo, prefill, onSubmit, onAnswer }) {
10
48
  const [input, setInput] = useState("");
49
+ useEffect(() => {
50
+ if (prefill)
51
+ setInput(prefill.text);
52
+ }, [prefill?.nonce]);
11
53
  const [scrollBack, setScrollBack] = useState(0);
12
54
  const { frame } = useAnimation({ interval: 80, isActive: !!busy });
13
- const inner = width - 2;
14
- const lines = useMemo(() => {
15
- const out = [];
16
- const all = streaming ? [...entries, { kind: "assistant", text: streaming }] : entries;
17
- for (const entry of all) {
18
- if (entry.kind === "user") {
19
- out.push({ text: "you", color: C.cyan, bold: true, bar: C.cyan });
20
- for (const line of wrap(entry.text, inner - 3))
21
- out.push({ text: line, color: C.text, bar: C.cyan });
22
- out.push({ text: "" });
23
- }
24
- else if (entry.kind === "assistant") {
25
- out.push({ text: "◆ nou", color: C.accent2, bold: true, bar: C.accent2 });
26
- for (const line of renderMarkdown(entry.text, inner - 3)) {
27
- const color = line.kind === "heading" ? C.accent2 : line.kind === "quote" ? C.dim : undefined;
28
- out.push({ text: "", spans: line.spans, ...(color ? { color } : {}), bar: C.accent2 });
29
- }
30
- out.push({ text: "" });
31
- }
32
- else {
33
- const style = entry.kind === "tool"
34
- ? { icon: "⚙", color: C.dim }
35
- : entry.kind === "error" ? { icon: "✖", color: C.red } : { icon: "ℹ", color: C.accent };
36
- wrap(entry.text, inner - 4).forEach((line, i) => out.push({ text: `${i === 0 ? ` ${style.icon}` : " "} ${line}`, color: style.color }));
37
- }
38
- }
39
- return out;
40
- }, [entries, streaming, inner]);
41
- // New output snaps the view back to the bottom.
42
- useEffect(() => setScrollBack(0), [entries.length, streaming]);
55
+ const inner = width - 3; // minus the borders and the scrollbar
56
+ const history = useMemo(() => entries.flatMap((e) => cachedLines(e, inner)), [entries, inner]);
57
+ const lines = useMemo(() => (streaming ? [...history, ...entryLines({ kind: "assistant", text: streaming }, inner)] : history), [history, streaming, inner]);
58
+ // At the bottom, new output scrolls into view. Scrolled up to read, the view stays where it is
59
+ // (counted from the bottom, so it moves up by however many lines arrived) until you scroll back down.
60
+ const lastCount = useRef(lines.length);
61
+ const [unread, setUnread] = useState(0);
62
+ useEffect(() => {
63
+ const added = lines.length - lastCount.current;
64
+ lastCount.current = lines.length;
65
+ if (added <= 0)
66
+ return;
67
+ setScrollBack((b) => (b > 0 ? b + added : 0));
68
+ setUnread((u) => (scrollBack > 0 ? u + added : 0));
69
+ }, [lines.length]);
70
+ // Sending a message always goes back to the bottom.
71
+ useEffect(() => {
72
+ if (entries[entries.length - 1]?.kind === "user")
73
+ setScrollBack(0);
74
+ }, [entries.length]);
75
+ useEffect(() => {
76
+ if (scrollBack === 0)
77
+ setUnread(0);
78
+ }, [scrollBack]);
43
79
  const footer = [];
44
80
  if (approval?.kind === "command") {
45
81
  footer.push({ text: "⚡ Run this command?", color: C.yellow, bold: true });
@@ -68,16 +104,21 @@ export function Chat({ width, height, focused, entries, streaming, busy, approva
68
104
  const suggestions = input.startsWith("/") && !input.includes(" ")
69
105
  ? COMMANDS.filter((c) => c.startsWith(input) && c !== input)
70
106
  : [];
71
- const inputRows = inputHeight(input, inner);
107
+ const inputRows = inputHeight(input, inner + 1);
72
108
  const rows = Math.max(1, height - 3 - footer.length - 2 - inputRows);
73
- const history = useMemo(() => entries.filter((e) => e.kind === "user").map((e) => e.text), [entries]);
109
+ const sent = useMemo(() => entries.filter((e) => e.kind === "user").map((e) => e.text), [entries]);
74
110
  const maxBack = Math.max(0, lines.length - rows);
75
111
  const back = Math.min(scrollBack, maxBack);
76
- const visible = lines.slice(Math.max(0, lines.length - rows - back), lines.length - back);
112
+ const firstShown = Math.max(0, lines.length - rows - back);
113
+ const visible = lines.slice(firstShown, lines.length - back);
77
114
  useEffect(() => {
78
- if (wheel)
79
- setScrollBack((b) => Math.max(0, Math.min(maxBack, b - wheel.dir * 3)));
115
+ if (wheel?.lines)
116
+ setScrollBack((b) => Math.max(0, Math.min(maxBack, Math.min(b, maxBack) - wheel.lines)));
80
117
  }, [wheel?.nonce]);
118
+ useEffect(() => {
119
+ if (scrollTo)
120
+ setScrollBack(maxBack - topFor(scrollTo.fraction, lines.length, rows));
121
+ }, [scrollTo?.nonce]);
81
122
  // While something waits for approval, y/n into an empty input answer it; any other key starts a message.
82
123
  useInput((typed) => {
83
124
  if (/^[yYnN]$/.test(typed))
@@ -87,9 +128,13 @@ export function Chat({ width, height, focused, entries, streaming, busy, approva
87
128
  }, { isActive: focused && !!approval && !input });
88
129
  useInput((_input, key) => {
89
130
  if (key.pageUp || (key.upArrow && key.shift))
90
- setScrollBack((b) => Math.min(maxBack, b + Math.max(1, rows - 2)));
131
+ setScrollBack((b) => Math.min(maxBack, Math.min(b, maxBack) + Math.max(1, rows - 2)));
91
132
  else if (key.pageDown || (key.downArrow && key.shift))
92
- setScrollBack((b) => Math.max(0, b - Math.max(1, rows - 2)));
133
+ setScrollBack((b) => Math.max(0, Math.min(b, maxBack) - Math.max(1, rows - 2)));
134
+ else if (key.ctrl && key.home)
135
+ setScrollBack(maxBack);
136
+ else if (key.ctrl && key.end)
137
+ setScrollBack(0);
93
138
  }, { isActive: focused });
94
139
  const submit = (value) => {
95
140
  if (!value.trim() || busy || approval)
@@ -99,14 +144,16 @@ export function Chat({ width, height, focused, entries, streaming, busy, approva
99
144
  setInput("");
100
145
  onSubmit(command);
101
146
  };
102
- const render = (line, key) => (_jsxs(Text, { wrap: "truncate-end", children: [line.bar ? _jsx(Text, { color: line.bar, children: "\u258C " }) : null, line.spans?.length
147
+ const render = (line, key, withLead = true) => (_jsxs(Text, { wrap: "truncate-end", children: [withLead && line.lead ? _jsx(Text, { color: line.leadColor ?? C.dim, children: line.lead }) : null, line.spans?.length
103
148
  ? line.spans.map((span, i) => (_jsx(Text, { bold: !!span.bold || !!line.bold, italic: !!span.italic, dimColor: !!span.dim, ...(span.code ? { color: C.yellow } : line.color ? { color: line.color } : { color: C.text }), children: span.text }, i)))
104
149
  : _jsx(Text, { bold: !!line.bold, dimColor: !!line.dim, ...(line.color ? { color: line.color } : {}), children: line.text || " " })] }, key));
105
150
  const empty = entries.length === 0 && !streaming;
106
151
  const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: C.accent2, children: "\u25C6" }), " Chat", back
107
- ? _jsxs(Text, { color: C.yellow, children: [" \u2191 scrolled ", back, " lines \u00B7 Shift+\u2193 / PgDn / wheel to go back"] })
108
- : maxBack > 0 ? _jsx(Text, { color: C.dim, children: " Shift+\u2191 / PgUp / wheel to scroll" }) : null] }));
109
- return (_jsxs(Pane, { title: title, width: width, height: height, focused: focused, children: [_jsx(Box, { flexDirection: "column", height: rows, children: empty ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: C.dim, children: "Try:" }), _jsx(Text, { color: C.text, children: " \u201Cexplain this file\u201D" }), _jsx(Text, { color: C.text, children: " \u201Cwhat changed since my last commit?\u201D" }), _jsx(Text, { color: C.text, children: " \u201Cadd error handling here\u201D" }), _jsx(Text, { children: " " }), _jsx(Text, { color: C.dim, children: "I can see the file you have open." })] })) : visible.map((line, i) => render(line, i)) }), _jsx(Box, { flexDirection: "column", children: footer.map((line, i) => render(line, `f${i}`)) }), _jsx(Text, { wrap: "truncate-end", color: C.dim, children: suggestions.length
152
+ ? unread
153
+ ? _jsxs(Text, { color: C.green, bold: true, children: [" \u2193 ", unread, " new lines below", _jsx(Text, { color: C.dim, children: " \u00B7 PgDn / wheel / Ctrl+End" })] })
154
+ : _jsxs(Text, { color: C.yellow, children: [" \u2191 scrolled ", back, " lines", _jsx(Text, { color: C.dim, children: " \u00B7 PgDn / wheel / Ctrl+End" })] })
155
+ : maxBack > 0 ? _jsx(Text, { color: C.dim, children: " PgUp / wheel to scroll" }) : null] }));
156
+ return (_jsxs(Pane, { title: title, width: width, height: height, focused: focused, children: [_jsxs(Box, { flexDirection: "row", height: rows, children: [_jsx(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: empty ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: C.dim, children: "Try:" }), _jsx(Text, { color: C.text, children: " \u201Cexplain this file\u201D" }), _jsx(Text, { color: C.text, children: " \u201Cwhat changed since my last commit?\u201D" }), _jsx(Text, { color: C.text, children: " \u201Cadd error handling here\u201D" }), _jsx(Text, { children: " " }), _jsx(Text, { color: C.dim, children: "I can see the file you have open." })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { children: visible.map((line, i) => _jsx(Text, { color: line.leadColor ?? C.dim, children: `${line.lead ?? " "}${i < visible.length - 1 ? "\n" : ""}` }, i)) }) }), _jsx(Box, { flexDirection: "column", width: inner - 2, flexShrink: 0, children: visible.map((line, i) => render(line, firstShown + i, false)) })] })) }), _jsx(Scrollbar, { total: lines.length, top: firstShown, rows: rows, focused: focused })] }), _jsx(Box, { flexDirection: "column", children: footer.map((line, i) => render(line, `f${i}`)) }), _jsx(Text, { wrap: "truncate-end", color: C.dim, children: suggestions.length
110
157
  ? _jsxs(Text, { color: C.accent, children: [suggestions.join(" "), " ", _jsx(Text, { color: C.dim, children: suggestions.length === 1 ? "Enter ↵" : "" })] })
111
- : contextChip ? _jsxs(_Fragment, { children: [_jsx(Text, { color: C.cyan, children: "@" }), " ", contextChip] }) : " " }), _jsx(Text, { color: focused ? C.accent : C.dim, children: "─".repeat(Math.max(0, inner)) }), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: submit, focus: focused && !(approval && !input), width: inner, history: history, placeholder: busy ? "working… Esc to stop" : "Ask anything · / for commands · ↑ history" })] }));
112
- }
158
+ : contextChip ? _jsxs(_Fragment, { children: [_jsx(Text, { color: C.cyan, children: "@" }), " ", contextChip] }) : " " }), _jsx(Text, { color: focused ? C.accent : C.dim, children: "─".repeat(Math.max(0, inner + 1)) }), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: submit, focus: focused && !(approval && !input), width: inner + 1, history: sent, placeholder: busy ? "working… Esc to stop" : "Ask anything · / for commands · ↑ history" })] }));
159
+ });
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { Box, Text, useInput, usePaste } from "ink";
4
4
  import { C } from "./theme.js";
5
5
  /** Word-wraps `text` into segments of at most `width` characters, honoring newlines. */
@@ -39,11 +39,20 @@ export function ChatInput({ value, onChange, onSubmit, focus, width, placeholder
39
39
  const [cursor, setCursor] = useState(value.length);
40
40
  const [recall, setRecall] = useState(null);
41
41
  const stash = useRef("");
42
+ // Text put in from outside (e.g. "/" typed in another pane) puts the cursor at its end.
43
+ const own = useRef(value);
44
+ useEffect(() => {
45
+ if (value !== own.current)
46
+ setCursor(value.length);
47
+ own.current = value;
48
+ }, [value]);
42
49
  const textWidth = Math.max(4, width - 2);
43
- const pos = Math.min(cursor, value.length);
50
+ // Until the effect above catches up, text from outside puts the cursor at its end right away.
51
+ const pos = value !== own.current ? value.length : Math.min(cursor, value.length);
44
52
  const segments = layout(value, textWidth);
45
53
  const line = cursorLine(segments, pos);
46
54
  const set = (next, nextCursor) => {
55
+ own.current = next;
47
56
  onChange(next);
48
57
  setCursor(Math.max(0, Math.min(next.length, nextCursor)));
49
58
  };
@@ -1,16 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
2
+ import { memo, useEffect, useMemo, useState } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { SEVERITY_STYLE } from "./ProblemsView.js";
5
5
  import { artWork } from "../util/index.js";
6
- import { highlightLines, sideBySide } from "./text.js";
6
+ import { highlightLines, sideBySide, splitLines } from "./text.js";
7
7
  import { C, fileIcon, gradient } from "./theme.js";
8
8
  import { Pane } from "./Pane.js";
9
- export function CodeView({ width, height, focused, filePath, content, diff, diffLabel, pending, marked, problems, focusLine, syntaxVersion, diffNote, wheel, onViewport }) {
9
+ import { Scrollbar, topFor } from "./scroll.js";
10
+ /** Columns ←/→ and Shift+wheel move sideways. */
11
+ const SIDE_STEP = 8;
12
+ export const CodeView = memo(function CodeView({ width, height, focused, filePath, content, diff, diffLabel, pending, marked, problems, focusLine, syntaxVersion, diffNote, wheel, scrollTo, onViewport }) {
10
13
  const [top, setTop] = useState(0);
14
+ // Columns scrolled sideways. Scrolled lines show without colors (slicing colored text could cut a color code in half).
15
+ const [left, setLeft] = useState(0);
11
16
  const rows = Math.max(1, height - 3);
12
- const inner = width - 2;
17
+ const inner = width - 3; // minus the borders and the scrollbar
13
18
  const lines = useMemo(() => (filePath ? highlightLines(filePath, content) : []), [filePath, content, syntaxVersion]);
19
+ const plain = useMemo(() => (left > 0 ? splitLines(content) : []), [content, left > 0]);
14
20
  const problemsByLine = useMemo(() => {
15
21
  const map = new Map();
16
22
  for (const d of problems)
@@ -25,6 +31,7 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
25
31
  ? diffRows.findIndex((r) => r.changed)
26
32
  : marked.size ? Math.min(...marked) - 1 : 0;
27
33
  setTop(Math.max(0, first - 3));
34
+ setLeft(0);
28
35
  }, [filePath, diff, diffRows, marked]);
29
36
  useEffect(() => {
30
37
  if (focusLine && !diff)
@@ -33,9 +40,17 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
33
40
  const maxTop = Math.max(0, total - rows);
34
41
  const start = Math.min(top, maxTop);
35
42
  useEffect(() => {
36
- if (wheel)
37
- setTop((t) => Math.max(0, Math.min(maxTop, Math.min(t, maxTop) + wheel.dir * 3)));
43
+ if (!wheel)
44
+ return;
45
+ if (wheel.lines)
46
+ setTop((t) => Math.max(0, Math.min(maxTop, Math.min(t, maxTop) + wheel.lines)));
47
+ if (wheel.columns)
48
+ setLeft((l) => Math.max(0, l + wheel.columns * 2));
38
49
  }, [wheel?.nonce]);
50
+ useEffect(() => {
51
+ if (scrollTo)
52
+ setTop(topFor(scrollTo.fraction, total, rows));
53
+ }, [scrollTo?.nonce]);
39
54
  useEffect(() => {
40
55
  if (filePath && !diff)
41
56
  onViewport(start + 1, Math.min(total, start + rows));
@@ -45,14 +60,18 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
45
60
  setTop((t) => Math.max(0, Math.min(t, maxTop) - 1));
46
61
  else if (key.downArrow || input === "j")
47
62
  setTop((t) => Math.min(maxTop, t + 1));
48
- else if (key.pageUp || input === "u")
63
+ else if (key.pageUp)
49
64
  setTop((t) => Math.max(0, Math.min(t, maxTop) - rows));
50
65
  else if (key.pageDown || input === "d" || input === " ")
51
66
  setTop((t) => Math.min(maxTop, t + rows));
52
- else if (input === "g")
67
+ else if (input === "g" || key.home)
53
68
  setTop(0);
54
- else if (input === "G")
69
+ else if (input === "G" || key.end)
55
70
  setTop(maxTop);
71
+ else if (key.leftArrow || input === "h")
72
+ setLeft((l) => Math.max(0, l - SIDE_STEP));
73
+ else if (key.rightArrow || input === "l")
74
+ setLeft((l) => l + SIDE_STEP);
56
75
  else if (diff && !pending && (input === "n" || input === "N")) {
57
76
  // Jump between change hunks.
58
77
  const starts = diffRows.flatMap((r, i) => (r.changed && !diffRows[i - 1]?.changed ? [i] : []));
@@ -61,7 +80,7 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
61
80
  setTop(Math.max(0, Math.min(maxTop, target - 3)));
62
81
  }
63
82
  }, { isActive: focused });
64
- const scroll = total > rows ? ` ${start + 1}-${Math.min(total, start + rows)}/${total}` : "";
83
+ const scroll = (total > rows ? ` ${start + 1}-${Math.min(total, start + rows)}/${total}` : "") + (left ? ` →${left}` : "");
65
84
  const percent = total > rows ? Math.round(((start + rows) / total) * 100) : 100;
66
85
  if (diff) {
67
86
  const half = Math.floor((inner - 1) / 2);
@@ -72,11 +91,11 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
72
91
  if (!c)
73
92
  return _jsx(Box, { width: w, children: _jsx(Text, { color: C.dim, wrap: "truncate-end", children: "╱".repeat(Math.max(0, w)) }) });
74
93
  const color = changed ? (side === "-" ? C.red : C.green) : undefined;
75
- return (_jsx(Box, { width: w, children: _jsxs(Text, { wrap: "truncate-end", ...(color ? { color } : { color: C.dim }), children: [String(c.n).padStart(gutter - 1), changed ? side : " ", c.text || " "] }) }));
94
+ return (_jsx(Box, { width: w, children: _jsxs(Text, { wrap: "truncate-end", ...(color ? { color } : { color: C.dim }), children: [String(c.n).padStart(gutter - 1), changed ? side : " ", c.text.slice(left) || " "] }) }));
76
95
  };
77
96
  const { icon, color } = fileIcon(diff.path);
78
97
  const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", diff.path, " ", _jsx(Text, { backgroundColor: pending ? C.yellow : C.accent2, color: "black", children: pending ? (diff.isNew ? " NEW FILE " : " PROPOSED ") : ` ${diffLabel.toUpperCase()} ` }), " ", _jsxs(Text, { color: C.green, children: ["+", added] }), " ", _jsxs(Text, { color: C.red, children: ["-", removed] }), diffNote ? _jsxs(Text, { color: diffNote.startsWith("✓") ? C.green : C.red, children: [" ", diffNote] }) : null, _jsxs(Text, { color: C.dim, children: [pending ? " y accept · n reject" : " n/N next/prev change", scroll] })] }));
79
- return (_jsx(Pane, { title: title, width: width, height: height, focused: focused, ...(pending ? { accent: C.yellow } : {}), children: _jsx(Box, { flexDirection: "column", children: diffRows.slice(start, start + rows).map((row, i) => (_jsxs(Box, { flexDirection: "row", children: [cell(row.left, "-", row.changed, half), _jsx(Text, { color: C.dim, children: "\u2502" }), cell(row.right, "+", row.changed, inner - half - 1)] }, start + i))) }) }));
98
+ return (_jsx(Pane, { title: title, width: width, height: height, focused: focused, ...(pending ? { accent: C.yellow } : {}), children: _jsxs(Box, { flexDirection: "row", height: rows, children: [_jsx(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: diffRows.slice(start, start + rows).map((row, i) => (_jsxs(Box, { flexDirection: "row", children: [cell(row.left, "-", row.changed, half), _jsx(Text, { color: C.dim, children: "\u2502" }), cell(row.right, "+", row.changed, inner - half - 1)] }, start + i))) }), _jsx(Scrollbar, { total: total, top: start, rows: rows, focused: focused })] }) }));
80
99
  }
81
100
  if (!filePath) {
82
101
  const tips = [
@@ -92,13 +111,13 @@ export function CodeView({ width, height, focused, filePath, content, diff, diff
92
111
  const { icon, color } = fileIcon(filePath);
93
112
  const errorCount = problems.filter((d) => d.severity === "error").length;
94
113
  const warningCount = problems.filter((d) => d.severity === "warning").length;
95
- const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath, errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \u2716 ", errorCount] }), warningCount > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", warningCount] }), _jsxs(Text, { color: C.dim, children: [scroll, total > rows ? ` (${percent}%)` : "", focused ? " e edit · v diff" : ""] })] }));
96
- return (_jsx(Pane, { title: title, width: width, height: height, focused: focused, children: _jsx(Box, { flexDirection: "column", children: lines.slice(start, start + rows).map((line, i) => {
97
- const n = start + i + 1;
98
- const isMarked = marked.has(n);
99
- const here = problemsByLine.get(n);
100
- const worst = here?.[0];
101
- const style = worst ? SEVERITY_STYLE[worst.severity] : null;
102
- return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: style?.color ?? (isMarked ? C.green : C.dim), children: String(n).padStart(gutter) }), style ? _jsx(Text, { color: style.color, children: style.icon }) : _jsx(Text, { color: isMarked ? C.green : C.dim, children: isMarked ? "▎" : " " }), line || " ", worst && style ? _jsxs(Text, { color: style.color, dimColor: true, children: [" ", style.icon, " ", worst.message, here.length > 1 ? ` (+${here.length - 1})` : ""] }) : null] }, n));
103
- }) }) }));
104
- }
114
+ const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath, errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \u2716 ", errorCount] }), warningCount > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", warningCount] }), _jsxs(Text, { color: C.dim, children: [scroll, total > rows ? ` (${percent}%)` : "", focused ? " click or e to edit · v diff" : ""] })] }));
115
+ return (_jsx(Pane, { title: title, width: width, height: height, focused: focused, children: _jsxs(Box, { flexDirection: "row", height: rows, children: [_jsx(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: lines.slice(start, start + rows).map((line, i) => {
116
+ const n = start + i + 1;
117
+ const isMarked = marked.has(n);
118
+ const here = problemsByLine.get(n);
119
+ const worst = here?.[0];
120
+ const style = worst ? SEVERITY_STYLE[worst.severity] : null;
121
+ return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: style?.color ?? (isMarked ? C.green : C.dim), children: String(n).padStart(gutter) }), style ? _jsx(Text, { color: style.color, children: style.icon }) : _jsx(Text, { color: isMarked ? C.green : C.dim, children: isMarked ? "▎" : " " }), (left ? plain[start + i]?.slice(left) : line) || " ", worst && style ? _jsxs(Text, { color: style.color, dimColor: true, children: [" ", style.icon, " ", worst.message, here.length > 1 ? ` (+${here.length - 1})` : ""] }) : null] }, n));
122
+ }) }), _jsx(Scrollbar, { total: total, top: start, rows: rows, focused: focused })] }) }));
123
+ });