revspec 0.8.3 → 0.8.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revspec",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Terminal-based spec review tool with real-time AI conversation",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/tui/app.ts CHANGED
@@ -108,6 +108,12 @@ export async function runTui(
108
108
  rootBox.add(bottomBar.box);
109
109
  renderer.root.add(rootBox);
110
110
 
111
+ // Wrap mode state
112
+ let wrapEnabled = false;
113
+ function currentWrapWidth(): number {
114
+ return wrapEnabled ? renderer.width : 0;
115
+ }
116
+
111
117
  // 7. Initial render
112
118
  function refreshPager(): void {
113
119
  // Spec mutation guard
@@ -118,7 +124,7 @@ export async function runTui(
118
124
  }
119
125
  } catch {}
120
126
 
121
- buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds);
127
+ buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds, currentWrapWidth());
122
128
  buildTopBar(topBar, specFile, state, state.unreadCount(), specMtimeChanged);
123
129
  // Don't overwrite transient messages (welcome hint, warnings) during navigation
124
130
  if (!messageTimer) {
@@ -171,7 +177,7 @@ export async function runTui(
171
177
  // Map visual row back to spec line number (for H/M/L)
172
178
  function visualRowToSpecLine(targetRow: number): number {
173
179
  for (let i = 0; i < state.specLines.length; i++) {
174
- const row = i + countExtraVisualLines(state.specLines, i);
180
+ const row = i + countExtraVisualLines(state.specLines, i, currentWrapWidth());
175
181
  if (row >= targetRow) return i + 1;
176
182
  }
177
183
  return state.lineCount;
@@ -219,7 +225,7 @@ export async function runTui(
219
225
  // Helper: scroll pager to ensure cursor line is visible
220
226
  function ensureCursorVisible(): void {
221
227
  // Map spec line to visual row, accounting for table border extra lines
222
- const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
228
+ const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1, currentWrapWidth());
223
229
  const cursorRow = state.cursorLine - 1 + extra;
224
230
  const viewportHeight = Math.max(1, renderer.height - 2); // minus top + bottom bar
225
231
 
@@ -254,6 +260,13 @@ export async function runTui(
254
260
  exitTui(resolve, "session-end");
255
261
  return "exit";
256
262
  }
263
+ // :wrap — toggle line wrapping
264
+ if (cmd === "wrap") {
265
+ wrapEnabled = !wrapEnabled;
266
+ refreshPager();
267
+ showTransient(wrapEnabled ? "Line wrap on" : "Line wrap off", "info");
268
+ return "stay";
269
+ }
257
270
  // :{N} — jump to line number
258
271
  const lineNum = parseInt(cmd, 10);
259
272
  if (!isNaN(lineNum) && lineNum > 0) {
@@ -636,7 +649,7 @@ export async function runTui(
636
649
  refreshPager();
637
650
  break;
638
651
  case "center-cursor": {
639
- const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
652
+ const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1, currentWrapWidth());
640
653
  const cursorRow = state.cursorLine - 1 + extra;
641
654
  const halfView = Math.floor(pageSize() / 2);
642
655
  pager.scrollBox.scrollTo(Math.max(0, cursorRow - halfView));
package/src/tui/help.ts CHANGED
@@ -114,6 +114,7 @@ export function createHelp(opts: {
114
114
  " :q/:wq Quit (warns if unresolved)",
115
115
  " :q! Force quit",
116
116
  " :{N} Jump to line N",
117
+ " :wrap Toggle line wrapping",
117
118
  " Ctrl+C Force quit",
118
119
  ]);
119
120
 
package/src/tui/pager.ts CHANGED
@@ -53,13 +53,33 @@ export function buildPagerContent(state: ReviewState, searchQuery?: string | nul
53
53
  * Inline markdown is parsed and styled per line.
54
54
  * Line numbers and thread hints are dimmed.
55
55
  */
56
- export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, searchQuery?: string | null, unreadThreadIds?: ReadonlySet<string>): void {
56
+ /**
57
+ * Word-wrap a string at the given width, breaking at word boundaries.
58
+ */
59
+ function wordWrap(text: string, width: number): string[] {
60
+ if (width <= 0 || text.length <= width) return [text];
61
+ const lines: string[] = [];
62
+ let remaining = text;
63
+ while (remaining.length > width) {
64
+ let breakAt = remaining.lastIndexOf(" ", width);
65
+ if (breakAt <= 0) breakAt = width; // no space found — hard break
66
+ lines.push(remaining.slice(0, breakAt));
67
+ remaining = remaining.slice(breakAt).replace(/^ /, ""); // trim leading space on continuation
68
+ }
69
+ if (remaining.length > 0) lines.push(remaining);
70
+ return lines;
71
+ }
72
+
73
+ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, searchQuery?: string | null, unreadThreadIds?: ReadonlySet<string>, wrapWidth?: number): void {
57
74
  lineNode.clear();
58
75
 
59
76
  // Calculate dynamic gutter width based on total line count
60
77
  const numWidth = Math.max(String(state.lineCount).length, 3);
61
- // Blank gutter for table borders: prefix(1) + indicator(1) + numWidth + spaces(2)
62
- const gutterBlank = " ".repeat(2 + numWidth + 2);
78
+ const gutterWidth = 2 + numWidth + 2; // prefix(1) + indicator(1) + numWidth + spaces(2)
79
+ // Blank gutter for table borders and wrap continuations
80
+ const gutterBlank = " ".repeat(gutterWidth);
81
+ // Available content width for wrapping
82
+ const contentWidth = wrapWidth && wrapWidth > gutterWidth ? wrapWidth - gutterWidth : 0;
63
83
 
64
84
  // Pre-scan for table blocks so we can calculate column widths
65
85
  const tableBlocks = new Map<number, TableBlock>();
@@ -179,6 +199,17 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
179
199
  lineNode.add(TextNodeRenderable.fromString(part, { fg: theme.text, bg: isCursor ? theme.backgroundElement : undefined }));
180
200
  }
181
201
  }
202
+ } else if (contentWidth > 0 && specText.length > contentWidth) {
203
+ // Wrap long lines — first chunk gets markdown, continuations get blank gutter + markdown
204
+ const chunks = wordWrap(specText, contentWidth);
205
+ const segments = parseMarkdownLine(chunks[0]);
206
+ addSegments(lineNode, segments, theme.text, isCursor ? theme.backgroundElement : undefined);
207
+ for (let c = 1; c < chunks.length; c++) {
208
+ lineNode.add(TextNodeRenderable.fromString("\n", {}));
209
+ lineNode.add(TextNodeRenderable.fromString(gutterBlank, { fg: theme.textDim }));
210
+ const contSegments = parseMarkdownLine(chunks[c]);
211
+ addSegments(lineNode, contSegments, theme.text, isCursor ? theme.backgroundElement : undefined);
212
+ }
182
213
  } else {
183
214
  // Parse and render inline markdown
184
215
  const segments = parseMarkdownLine(specText);
@@ -195,10 +226,14 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
195
226
  // --- Visual row offset calculation ---
196
227
 
197
228
  /**
198
- * Count extra visual lines (table borders) before a given spec line index.
229
+ * Count extra visual lines (table borders + word wrap) before a given spec line index.
199
230
  * Used to map spec line numbers to actual visual rows in the rendered content.
200
231
  */
201
- export function countExtraVisualLines(specLines: string[], cursorIndex: number): number {
232
+ export function countExtraVisualLines(specLines: string[], cursorIndex: number, wrapWidth?: number): number {
233
+ const numWidth = Math.max(String(specLines.length).length, 3);
234
+ const gutterWidth = 2 + numWidth + 2;
235
+ const contentWidth = wrapWidth && wrapWidth > gutterWidth ? wrapWidth - gutterWidth : 0;
236
+
202
237
  let extra = 0;
203
238
  let i = 0;
204
239
  while (i < specLines.length) {
@@ -215,6 +250,10 @@ export function countExtraVisualLines(specLines: string[], cursorIndex: number):
215
250
  if (cursorIndex >= tableEnd) extra++;
216
251
  continue;
217
252
  }
253
+ // Word wrap: count extra continuation lines
254
+ if (contentWidth > 0 && i < cursorIndex && specLines[i].length > contentWidth) {
255
+ extra += wordWrap(specLines[i], contentWidth).length - 1;
256
+ }
218
257
  i++;
219
258
  }
220
259
  return extra;
@@ -243,7 +282,7 @@ export function createPager(renderer: CliRenderer): PagerComponents {
243
282
  width: "100%",
244
283
  flexGrow: 1,
245
284
  scrollY: true,
246
- scrollX: true,
285
+ scrollX: false,
247
286
  backgroundColor: theme.base,
248
287
  });
249
288
 
@@ -82,7 +82,7 @@ export function parseMarkdownLine(line: string): StyledSegment[] {
82
82
  const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
83
83
  if (headingMatch) {
84
84
  const level = headingMatch[1].length;
85
- const color = level <= 2 ? theme.blue : theme.mauve;
85
+ const color = level <= 2 ? theme.blue : level === 3 ? theme.mauve : theme.textMuted;
86
86
  // Parse inline markdown within heading text
87
87
  const inner = parseInlineMarkdown(headingMatch[2]);
88
88
  return inner.map((s) => ({