revspec 0.3.0 → 0.4.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.
package/src/tui/pager.ts CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  TextAttributes,
8
8
  type CliRenderer,
9
9
  } from "@opentui/core";
10
- import { theme } from "./theme";
10
+ import { theme } from "./ui/theme";
11
+ import { parseMarkdownLine, addSegments, collectTable, renderTableBorder, renderTableSeparator, renderTableRow, parseTableCells, type TableBlock } from "./ui/markdown";
11
12
 
12
13
  const MAX_HINT_LENGTH = 40;
13
14
 
@@ -25,250 +26,6 @@ function threadHint(thread: Thread): string {
25
26
  return text.slice(0, MAX_HINT_LENGTH - 1) + "\u2026";
26
27
  }
27
28
 
28
- // --- Inline markdown parser ---
29
-
30
- interface StyledSegment {
31
- text: string;
32
- fg?: string;
33
- attributes?: number;
34
- }
35
-
36
- /**
37
- * Parse inline markdown (bold, italic, code) into styled segments.
38
- * Strips syntax markers and returns display text with style info.
39
- */
40
- function parseInlineMarkdown(text: string): StyledSegment[] {
41
- const segments: StyledSegment[] = [];
42
- // Order matters: **bold** before *italic*
43
- const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`)/g;
44
- let pos = 0;
45
- let match;
46
- while ((match = regex.exec(text)) !== null) {
47
- if (match.index > pos) {
48
- segments.push({ text: text.slice(pos, match.index) });
49
- }
50
- if (match[2] !== undefined) {
51
- // **bold**
52
- segments.push({ text: match[2], attributes: TextAttributes.BOLD });
53
- } else if (match[3] !== undefined) {
54
- // *italic*
55
- segments.push({ text: match[3], attributes: TextAttributes.ITALIC });
56
- } else if (match[4] !== undefined) {
57
- // `code`
58
- segments.push({ text: match[4], fg: theme.green });
59
- }
60
- pos = match.index + match[0].length;
61
- }
62
- if (pos < text.length) {
63
- segments.push({ text: text.slice(pos) });
64
- }
65
- if (segments.length === 0) {
66
- segments.push({ text });
67
- }
68
- return segments;
69
- }
70
-
71
- /**
72
- * Parse a full line of markdown into styled segments.
73
- * Handles block-level syntax (headings, lists, blockquotes, hr)
74
- * and delegates inline content to parseInlineMarkdown.
75
- */
76
- function parseMarkdownLine(line: string): StyledSegment[] {
77
- // Heading: # ... ###### (strip markers, bold + colored)
78
- const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
79
- if (headingMatch) {
80
- const level = headingMatch[1].length;
81
- const color = level <= 2 ? theme.blue : theme.mauve;
82
- // Parse inline markdown within heading text
83
- const inner = parseInlineMarkdown(headingMatch[2]);
84
- return inner.map((s) => ({
85
- ...s,
86
- fg: s.fg ?? color,
87
- attributes: (s.attributes ?? 0) | TextAttributes.BOLD,
88
- }));
89
- }
90
-
91
- // Horizontal rule: --- or *** or ___
92
- if (/^(\s*[-*_]\s*){3,}$/.test(line)) {
93
- return [{ text: "\u2500".repeat(40), fg: theme.overlay, attributes: TextAttributes.DIM }];
94
- }
95
-
96
- // Blockquote: > text
97
- if (line.startsWith("> ")) {
98
- const inner = parseInlineMarkdown(line.slice(2));
99
- return [
100
- { text: "\u2502 ", fg: theme.overlay },
101
- ...inner.map((s) => ({
102
- ...s,
103
- fg: s.fg ?? theme.overlay,
104
- attributes: (s.attributes ?? 0) | TextAttributes.ITALIC,
105
- })),
106
- ];
107
- }
108
-
109
- // Unordered list: - item, * item, + item
110
- const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
111
- if (ulMatch) {
112
- return [
113
- { text: ulMatch[1] + "\u2022 ", fg: theme.yellow },
114
- ...parseInlineMarkdown(ulMatch[3]),
115
- ];
116
- }
117
-
118
- // Ordered list: 1. item
119
- const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/);
120
- if (olMatch) {
121
- return [
122
- { text: `${olMatch[1]}${olMatch[2]}. `, fg: theme.yellow },
123
- ...parseInlineMarkdown(olMatch[3]),
124
- ];
125
- }
126
-
127
- // Regular line — parse inline markdown only
128
- return parseInlineMarkdown(line);
129
- }
130
-
131
- /**
132
- * Add styled segments as TextNodeRenderable children to a parent.
133
- */
134
- function addSegments(parent: TextRenderable, segments: StyledSegment[], defaultFg: string): void {
135
- for (const seg of segments) {
136
- const node = TextNodeRenderable.fromString(seg.text, {
137
- fg: seg.fg ?? defaultFg,
138
- attributes: seg.attributes,
139
- });
140
- parent.add(node);
141
- }
142
- }
143
-
144
- // --- Table rendering ---
145
-
146
- const SEPARATOR_RE = /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|?\s*$/;
147
-
148
- /** Split a table row into trimmed cell values (strips outer pipes). */
149
- function parseTableCells(line: string): string[] {
150
- const trimmed = line.trim();
151
- // Remove leading/trailing pipes and split
152
- const inner = trimmed.startsWith("|") ? trimmed.slice(1) : trimmed;
153
- const withoutTrailing = inner.endsWith("|") ? inner.slice(0, -1) : inner;
154
- return withoutTrailing.split("|").map((c) => c.trim());
155
- }
156
-
157
- /** Compute the display width of a string (strips inline markdown markers). */
158
- function displayWidth(text: string): number {
159
- // Remove **bold**, *italic*, `code` markers to get display length
160
- return text
161
- .replace(/\*\*(.+?)\*\*/g, "$1")
162
- .replace(/\*(.+?)\*/g, "$1")
163
- .replace(/`([^`]+)`/g, "$1")
164
- .length;
165
- }
166
-
167
- interface TableBlock {
168
- startIndex: number;
169
- lines: string[];
170
- separatorIndex: number; // relative to startIndex, -1 if none
171
- colWidths: number[];
172
- }
173
-
174
- /** Scan ahead from a starting `|` line and collect the full table block. */
175
- function collectTable(specLines: string[], start: number): TableBlock {
176
- const lines: string[] = [];
177
- let i = start;
178
- while (i < specLines.length && specLines[i].trimStart().startsWith("|")) {
179
- lines.push(specLines[i]);
180
- i++;
181
- }
182
-
183
- // Find separator row
184
- let separatorIndex = -1;
185
- for (let j = 0; j < lines.length; j++) {
186
- if (SEPARATOR_RE.test(lines[j])) {
187
- separatorIndex = j;
188
- break;
189
- }
190
- }
191
-
192
- // Calculate column widths from all non-separator rows
193
- const allCells = lines
194
- .filter((_, j) => j !== separatorIndex)
195
- .map(parseTableCells);
196
- const maxCols = Math.max(...allCells.map((r) => r.length), 0);
197
- const colWidths: number[] = new Array(maxCols).fill(0);
198
- for (const row of allCells) {
199
- for (let c = 0; c < row.length; c++) {
200
- colWidths[c] = Math.max(colWidths[c], displayWidth(row[c]));
201
- }
202
- }
203
- // Minimum width of 3 per column
204
- for (let c = 0; c < colWidths.length; c++) {
205
- colWidths[c] = Math.max(colWidths[c], 3);
206
- }
207
-
208
- return { startIndex: start, lines, separatorIndex, colWidths };
209
- }
210
-
211
- /** Render a table separator row with box-drawing characters. */
212
- function renderTableSeparator(parent: TextRenderable, colWidths: number[]): void {
213
- const parts = colWidths.map((w) => "\u2500".repeat(w + 2));
214
- const line = "\u251c" + parts.join("\u253c") + "\u2524";
215
- parent.add(TextNodeRenderable.fromString(line, { fg: theme.overlay }));
216
- }
217
-
218
- /** Render a table data/header row with padded, styled cells. */
219
- function renderTableRow(
220
- parent: TextRenderable,
221
- cells: string[],
222
- colWidths: number[],
223
- isHeader: boolean,
224
- ): void {
225
- for (let c = 0; c < colWidths.length; c++) {
226
- const cellText = c < cells.length ? cells[c] : "";
227
- const dw = displayWidth(cellText);
228
- const padding = Math.max(0, colWidths[c] - dw);
229
-
230
- // Left border
231
- parent.add(TextNodeRenderable.fromString(
232
- c === 0 ? "\u2502 " : " \u2502 ",
233
- { fg: theme.overlay }
234
- ));
235
-
236
- // Cell content — parse inline markdown, apply header bold
237
- const segments = parseInlineMarkdown(cellText);
238
- for (const seg of segments) {
239
- const attrs = isHeader
240
- ? (seg.attributes ?? 0) | TextAttributes.BOLD
241
- : seg.attributes;
242
- parent.add(TextNodeRenderable.fromString(seg.text, {
243
- fg: seg.fg ?? theme.text,
244
- attributes: attrs,
245
- }));
246
- }
247
-
248
- // Padding
249
- if (padding > 0) {
250
- parent.add(TextNodeRenderable.fromString(" ".repeat(padding), {}));
251
- }
252
- }
253
- // Right border
254
- parent.add(TextNodeRenderable.fromString(
255
- " \u2502",
256
- { fg: theme.overlay }
257
- ));
258
- }
259
-
260
- /** Render a top or bottom border for the table. */
261
- function renderTableBorder(parent: TextRenderable, colWidths: number[], position: "top" | "bottom"): void {
262
- const [left, mid, right] = position === "top"
263
- ? ["\u250c", "\u252c", "\u2510"]
264
- : ["\u2514", "\u2534", "\u2518"];
265
- const parts = colWidths.map((w) => "\u2500".repeat(w + 2));
266
- parent.add(TextNodeRenderable.fromString(
267
- left + parts.join(mid) + right,
268
- { fg: theme.overlay }
269
- ));
270
- }
271
-
272
29
  // --- Plain text builder (for tests) ---
273
30
 
274
31
  /**
@@ -334,7 +91,7 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
334
91
 
335
92
  // Thread indicator — gutter bar on the left
336
93
  let indicator = " ";
337
- let indicatorColor = theme.overlay;
94
+ let indicatorColor: string = theme.textDim;
338
95
  if (thread) {
339
96
  const isUnread = unreadThreadIds && unreadThreadIds.has(thread.id);
340
97
  if (isUnread) {
@@ -356,7 +113,7 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
356
113
 
357
114
  // Top border before first table row (on its own visual line with blank gutter)
358
115
  if (isTable && relIdx === 0) {
359
- lineNode.add(TextNodeRenderable.fromString(" ", { fg: theme.overlay }));
116
+ lineNode.add(TextNodeRenderable.fromString(" ", { fg: theme.textDim }));
360
117
  renderTableBorder(lineNode, tableBlock.colWidths, "top");
361
118
  lineNode.add(TextNodeRenderable.fromString("\n", {}));
362
119
  }
@@ -364,15 +121,15 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
364
121
  // Gutter: cursor + indicator + line number (dimmed)
365
122
  lineNode.add(TextNodeRenderable.fromString(
366
123
  `${prefix}`,
367
- { fg: isCursor ? theme.text : theme.overlay }
124
+ { fg: isCursor ? theme.text : theme.textDim, bg: isCursor ? theme.backgroundElement : undefined }
368
125
  ));
369
126
  lineNode.add(TextNodeRenderable.fromString(
370
127
  indicator,
371
- { fg: indicatorColor }
128
+ { fg: indicatorColor, bg: isCursor ? theme.backgroundElement : undefined }
372
129
  ));
373
130
  lineNode.add(TextNodeRenderable.fromString(
374
131
  `${padLineNum(lineNum)} `,
375
- { fg: theme.overlay, attributes: TextAttributes.DIM }
132
+ { fg: theme.textDim, attributes: TextAttributes.DIM, bg: isCursor ? theme.backgroundElement : undefined }
376
133
  ));
377
134
 
378
135
  // Spec text — table or regular markdown
@@ -390,18 +147,28 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
390
147
  // Bottom border after last row (on its own visual line with blank gutter)
391
148
  if (relIdx === tableBlock.lines.length - 1) {
392
149
  lineNode.add(TextNodeRenderable.fromString("\n", {}));
393
- lineNode.add(TextNodeRenderable.fromString(" ", { fg: theme.overlay }));
150
+ lineNode.add(TextNodeRenderable.fromString(" ", { fg: theme.textDim }));
394
151
  renderTableBorder(lineNode, tableBlock.colWidths, "bottom");
395
152
  }
396
153
  } else if (searchQuery) {
397
- // When searching, show raw text with search markers (no markdown styling)
398
- const regex = new RegExp(searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
399
- const highlighted = specText.replace(regex, (match) => `>>${match}<<`);
400
- lineNode.add(TextNodeRenderable.fromString(highlighted, { fg: theme.text }));
154
+ // When searching, show colored match segments (no markdown styling)
155
+ const escaped = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
156
+ const searchRegex = new RegExp(`(${escaped})`, "gi");
157
+ const parts = specText.split(searchRegex);
158
+ for (let p = 0; p < parts.length; p++) {
159
+ const part = parts[p];
160
+ if (part.length === 0) continue;
161
+ const isMatch = p % 2 === 1; // split with capture group: [before, match, between, match, after]
162
+ if (isMatch) {
163
+ lineNode.add(TextNodeRenderable.fromString(part, { fg: theme.base, bg: theme.yellow, attributes: TextAttributes.BOLD }));
164
+ } else {
165
+ lineNode.add(TextNodeRenderable.fromString(part, { fg: theme.text, bg: isCursor ? theme.backgroundElement : undefined }));
166
+ }
167
+ }
401
168
  } else {
402
169
  // Parse and render inline markdown
403
170
  const segments = parseMarkdownLine(specText);
404
- addSegments(lineNode, segments, theme.text);
171
+ addSegments(lineNode, segments, theme.text, isCursor ? theme.backgroundElement : undefined);
405
172
  }
406
173
 
407
174
  // Thread hint (dimmed, inline)
@@ -409,7 +176,7 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
409
176
  const hint = threadHint(thread);
410
177
  lineNode.add(TextNodeRenderable.fromString(
411
178
  ` \u00ab ${hint}`,
412
- { fg: theme.overlay, attributes: TextAttributes.DIM }
179
+ { fg: theme.textDim, attributes: TextAttributes.DIM, bg: isCursor ? theme.backgroundElement : undefined }
413
180
  ));
414
181
  }
415
182
 
package/src/tui/search.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  type CliRenderer,
6
6
  type KeyEvent,
7
7
  } from "@opentui/core";
8
- import { theme } from "./theme";
8
+ import { theme } from "./ui/theme";
9
9
 
10
10
  export interface SearchOptions {
11
11
  renderer: CliRenderer;
@@ -36,7 +36,7 @@ export function createSearch(opts: SearchOptions): SearchOverlay {
36
36
  width: "100%",
37
37
  height: 1,
38
38
  zIndex: 100,
39
- backgroundColor: theme.surface0,
39
+ backgroundColor: theme.backgroundPanel,
40
40
  flexDirection: "row",
41
41
  alignItems: "center",
42
42
  });
@@ -47,7 +47,7 @@ export function createSearch(opts: SearchOptions): SearchOverlay {
47
47
  width: 3,
48
48
  height: 1,
49
49
  fg: theme.yellow,
50
- bg: theme.surface0,
50
+ bg: theme.backgroundPanel,
51
51
  wrapMode: "none",
52
52
  });
53
53
 
@@ -55,12 +55,12 @@ export function createSearch(opts: SearchOptions): SearchOverlay {
55
55
  const input = new InputRenderable(renderer, {
56
56
  width: "100%",
57
57
  flexGrow: 1,
58
- backgroundColor: theme.surface0,
58
+ backgroundColor: theme.backgroundPanel,
59
59
  textColor: theme.text,
60
- focusedBackgroundColor: theme.surface1,
60
+ focusedBackgroundColor: theme.backgroundElement,
61
61
  focusedTextColor: theme.text,
62
62
  placeholder: "Search...",
63
- placeholderColor: theme.overlay,
63
+ placeholderColor: theme.textDim,
64
64
  });
65
65
 
66
66
  container.add(label);
@@ -1,7 +1,8 @@
1
1
  import { BoxRenderable, TextRenderable, TextNodeRenderable, TextAttributes, type CliRenderer } from "@opentui/core";
2
2
  import type { ReviewState } from "../state/review-state";
3
3
  import { basename } from "path";
4
- import { theme } from "./theme";
4
+ import { theme } from "./ui/theme";
5
+ import { buildHints } from "./ui/hint-bar";
5
6
 
6
7
  export interface TopBarComponents {
7
8
  box: BoxRenderable;
@@ -31,7 +32,7 @@ export function buildTopBar(
31
32
  // Filename — bold
32
33
  t.add(TextNodeRenderable.fromString(` ${name}`, { fg: theme.text, attributes: TextAttributes.BOLD }));
33
34
 
34
- t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
35
+ t.add(TextNodeRenderable.fromString(" \u00b7 ", { fg: theme.textDim }));
35
36
 
36
37
  // Thread summary
37
38
  if (open > 0 || pending > 0) {
@@ -40,12 +41,12 @@ export function buildTopBar(
40
41
  if (pending > 0) parts.push(`${pending} pending`);
41
42
  t.add(TextNodeRenderable.fromString(parts.join(", "), { fg: theme.yellow }));
42
43
  } else {
43
- t.add(TextNodeRenderable.fromString("No active threads", { fg: theme.subtext }));
44
+ t.add(TextNodeRenderable.fromString("No active threads", { fg: theme.textMuted }));
44
45
  }
45
46
 
46
47
  // Unread replies
47
48
  if (unreadCount && unreadCount > 0) {
48
- t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
49
+ t.add(TextNodeRenderable.fromString(" \u00b7 ", { fg: theme.textDim }));
49
50
  t.add(TextNodeRenderable.fromString(
50
51
  `${unreadCount} new repl${unreadCount === 1 ? "y" : "ies"}`,
51
52
  { fg: theme.green, attributes: TextAttributes.BOLD }
@@ -54,13 +55,13 @@ export function buildTopBar(
54
55
 
55
56
  // Spec changed warning
56
57
  if (specChanged) {
57
- t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
58
+ t.add(TextNodeRenderable.fromString(" \u00b7 ", { fg: theme.textDim }));
58
59
  t.add(TextNodeRenderable.fromString("!! Spec changed externally", { fg: theme.red, attributes: TextAttributes.BOLD }));
59
60
  }
60
61
 
61
62
  // Cursor position
62
- t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
63
- t.add(TextNodeRenderable.fromString(`L${state.cursorLine}/${state.lineCount}`, { fg: theme.subtext }));
63
+ t.add(TextNodeRenderable.fromString(" \u00b7 ", { fg: theme.textDim }));
64
+ t.add(TextNodeRenderable.fromString(`L${state.cursorLine}/${state.lineCount}`, { fg: theme.textMuted }));
64
65
  }
65
66
 
66
67
  /**
@@ -80,15 +81,7 @@ export function buildBottomBar(bar: BottomBarComponents, commandBuffer: string |
80
81
  { key: "/", action: "search" },
81
82
  { key: "?", action: "help" },
82
83
  ];
83
- t.add(TextNodeRenderable.fromString(" ", {}));
84
- for (let i = 0; i < hints.length; i++) {
85
- const h = hints[i];
86
- t.add(TextNodeRenderable.fromString(`[${h.key}]`, { fg: theme.blue }));
87
- t.add(TextNodeRenderable.fromString(` ${h.action}`, { fg: theme.subtext }));
88
- if (i < hints.length - 1) {
89
- t.add(TextNodeRenderable.fromString(" ", {}));
90
- }
91
- }
84
+ buildHints(t, hints);
92
85
  }
93
86
 
94
87
  /**
@@ -98,9 +91,7 @@ export function createTopBar(renderer: CliRenderer): TopBarComponents {
98
91
  const box = new BoxRenderable(renderer, {
99
92
  width: "100%",
100
93
  height: 1,
101
- backgroundColor: theme.base,
102
- border: ["bottom"],
103
- borderColor: theme.surface1,
94
+ backgroundColor: theme.backgroundPanel,
104
95
  });
105
96
 
106
97
  const text = new TextRenderable(renderer, {
@@ -123,7 +114,7 @@ export function createBottomBar(renderer: CliRenderer): BottomBarComponents {
123
114
  width: "100%",
124
115
  height: 1,
125
116
  flexShrink: 0,
126
- backgroundColor: theme.surface0,
117
+ backgroundColor: theme.backgroundPanel,
127
118
  });
128
119
 
129
120
  const text = new TextRenderable(renderer, {
@@ -1,13 +1,12 @@
1
1
  import {
2
- BoxRenderable,
3
2
  TextRenderable,
4
3
  SelectRenderable,
5
4
  SelectRenderableEvents,
6
5
  type CliRenderer,
7
- type KeyEvent,
8
6
  } from "@opentui/core";
9
7
  import type { Thread } from "../protocol/types";
10
- import { theme, STATUS_ICONS } from "./theme";
8
+ import { theme, STATUS_ICONS } from "./ui/theme";
9
+ import { createDialog } from "./ui/dialog";
11
10
 
12
11
  export interface ThreadListOptions {
13
12
  renderer: CliRenderer;
@@ -17,7 +16,7 @@ export interface ThreadListOptions {
17
16
  }
18
17
 
19
18
  export interface ThreadListOverlay {
20
- container: BoxRenderable;
19
+ container: import("@opentui/core").BoxRenderable;
21
20
  cleanup: () => void;
22
21
  }
23
22
 
@@ -44,21 +43,22 @@ export function createThreadList(opts: ThreadListOptions): ThreadListOverlay {
44
43
  (t) => t.status === "open" || t.status === "pending"
45
44
  );
46
45
 
47
- // Overlay container
48
- const container = new BoxRenderable(renderer, {
49
- position: "absolute",
50
- top: "15%",
51
- left: "15%",
46
+ const count = activeThreads.length;
47
+
48
+ const dialog = createDialog({
49
+ renderer,
50
+ title: `Threads (${count} active)`,
52
51
  width: "70%",
53
52
  height: "60%",
54
- zIndex: 100,
55
- backgroundColor: theme.base,
56
- border: true,
57
- borderStyle: "single",
58
- borderColor: theme.borderList,
59
- title: ` Threads (${activeThreads.length} active) `,
60
- flexDirection: "column",
61
- padding: 1,
53
+ top: "15%",
54
+ left: "15%",
55
+ borderColor: theme.mauve,
56
+ onDismiss: onCancel,
57
+ hints: [
58
+ { key: "j/k", action: "navigate" },
59
+ { key: "Enter", action: "jump" },
60
+ { key: "Esc", action: "close" },
61
+ ],
62
62
  });
63
63
 
64
64
  if (activeThreads.length === 0) {
@@ -66,10 +66,10 @@ export function createThreadList(opts: ThreadListOptions): ThreadListOverlay {
66
66
  content: "No active threads. Press [Esc] to close.",
67
67
  width: "100%",
68
68
  height: 1,
69
- fg: theme.overlay,
69
+ fg: theme.textDim,
70
70
  wrapMode: "none",
71
71
  });
72
- container.add(emptyMsg);
72
+ dialog.content.add(emptyMsg);
73
73
  } else {
74
74
  // Build select options from threads
75
75
  const selectOptions = activeThreads.map((t) => {
@@ -86,19 +86,19 @@ export function createThreadList(opts: ThreadListOptions): ThreadListOverlay {
86
86
  flexGrow: 1,
87
87
  options: selectOptions,
88
88
  selectedIndex: 0,
89
- backgroundColor: theme.base,
89
+ backgroundColor: theme.backgroundPanel,
90
90
  textColor: theme.text,
91
- focusedBackgroundColor: theme.base,
91
+ focusedBackgroundColor: theme.backgroundPanel,
92
92
  focusedTextColor: theme.text,
93
- selectedBackgroundColor: theme.surface1,
93
+ selectedBackgroundColor: theme.backgroundElement,
94
94
  selectedTextColor: "#f5c2e7",
95
- descriptionColor: theme.overlay,
96
- selectedDescriptionColor: theme.subtext,
95
+ descriptionColor: theme.textDim,
96
+ selectedDescriptionColor: theme.textMuted,
97
97
  showDescription: true,
98
98
  wrapSelection: true,
99
99
  });
100
100
 
101
- container.add(select);
101
+ dialog.content.add(select);
102
102
 
103
103
  // Focus the select so it handles j/k navigation
104
104
  renderer.focusRenderable(select);
@@ -112,34 +112,8 @@ export function createThreadList(opts: ThreadListOptions): ThreadListOverlay {
112
112
  });
113
113
  }
114
114
 
115
- // Hint bar
116
- const hint = new TextRenderable(renderer, {
117
- content: " [j/k] navigate [Enter] jump [Esc] close",
118
- width: "100%",
119
- height: 1,
120
- fg: theme.hintFg,
121
- bg: theme.hintBg,
122
- wrapMode: "none",
123
- truncate: true,
124
- });
125
-
126
- container.add(hint);
127
-
128
- // Key handler for Esc
129
- const keyHandler = (key: KeyEvent) => {
130
- if (key.name === "escape") {
131
- key.preventDefault();
132
- key.stopPropagation();
133
- onCancel();
134
- return;
135
- }
115
+ return {
116
+ container: dialog.container,
117
+ cleanup: dialog.cleanup,
136
118
  };
137
-
138
- renderer.keyInput.on("keypress", keyHandler);
139
-
140
- function cleanup(): void {
141
- renderer.keyInput.off("keypress", keyHandler);
142
- }
143
-
144
- return { container, cleanup };
145
119
  }