critique 0.1.33 → 0.1.34
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/AGENTS.md +1 -1
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/scripts/preview-review.tsx +2 -2
- package/src/ansi-html.ts +10 -10
- package/src/cli.tsx +9 -7
- package/src/review/acp-client.ts +3 -3
- package/src/review/diagram-parser.test.ts +327 -0
- package/src/review/diagram-parser.ts +229 -0
- package/src/review/review-app.test.tsx +101 -3
- package/src/review/review-app.tsx +98 -15
- package/src/themes/github.json +6 -0
- package/src/themes.ts +8 -6
package/AGENTS.md
CHANGED
|
@@ -8,7 +8,7 @@ ALWAYS!
|
|
|
8
8
|
|
|
9
9
|
### using unreleased opentui versions
|
|
10
10
|
|
|
11
|
-
to use a pkg.pr.new preview URL for opentui, get the last commit hash (
|
|
11
|
+
to use a pkg.pr.new preview URL for opentui, get the last commit hash (40 chars always) from PR https://github.com/anomalyco/opentui/pull/536:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
gh pr view 536 -R anomalyco/opentui --json commits --jq '.commits[-1].oid[:40]'
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# 0.1.34
|
|
2
|
+
|
|
3
|
+
- `review` command:
|
|
4
|
+
- Add diagram parser to extract and render diagrams from AI descriptions
|
|
5
|
+
- Show generating indicator below last hunk with animated spinner and dots
|
|
6
|
+
- Use "diagram" language in system prompt for code blocks
|
|
7
|
+
- Web preview:
|
|
8
|
+
- Fix text selection by switching from flex to block layout for lines
|
|
9
|
+
- UI:
|
|
10
|
+
- Fix layout shift in session multiselect by moving time ago to label
|
|
11
|
+
- Themes:
|
|
12
|
+
- Add "conceal" color support
|
|
13
|
+
|
|
1
14
|
# 0.1.33
|
|
2
15
|
|
|
3
16
|
- Dependencies:
|
package/package.json
CHANGED
|
@@ -121,7 +121,7 @@ This improves both security and developer experience when working with the API.
|
|
|
121
121
|
|
|
122
122
|
The new request flow with error handling:
|
|
123
123
|
|
|
124
|
-
\`\`\`
|
|
124
|
+
\`\`\`diagram
|
|
125
125
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
126
126
|
│ Client │────▶│ Router │────▶│ Handler │────▶│ Database │
|
|
127
127
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
@@ -182,7 +182,7 @@ Added test case for the new error handling behavior, ensuring that:
|
|
|
182
182
|
|
|
183
183
|
### Test Flow Diagram
|
|
184
184
|
|
|
185
|
-
\`\`\`
|
|
185
|
+
\`\`\`diagram
|
|
186
186
|
┌──────────────────────────────────────────────────────────────────┐
|
|
187
187
|
│ Test Suite │
|
|
188
188
|
└──────────────────────────────────────────────────────────────────┘
|
package/src/ansi-html.ts
CHANGED
|
@@ -32,7 +32,7 @@ function escapeHtml(text: string): string {
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Convert a single span to HTML
|
|
35
|
-
* Always wraps in
|
|
35
|
+
* Always wraps in span for consistent inline-block sizing
|
|
36
36
|
*/
|
|
37
37
|
function spanToHtml(span: TerminalSpan): string {
|
|
38
38
|
const styles: string[] = []
|
|
@@ -63,7 +63,7 @@ function spanToHtml(span: TerminalSpan): string {
|
|
|
63
63
|
|
|
64
64
|
const escapedText = escapeHtml(span.text)
|
|
65
65
|
|
|
66
|
-
// Always wrap in span for consistent
|
|
66
|
+
// Always wrap in span for consistent inline-block sizing
|
|
67
67
|
if (styles.length === 0) {
|
|
68
68
|
return `<span>${escapedText}</span>`
|
|
69
69
|
}
|
|
@@ -168,10 +168,7 @@ html, body {
|
|
|
168
168
|
color: ${textColor};
|
|
169
169
|
font-family: ${fontFamily};
|
|
170
170
|
font-size: ${fontSize};
|
|
171
|
-
line-height: 1.
|
|
172
|
-
-webkit-font-smoothing: antialiased;
|
|
173
|
-
-moz-osx-font-smoothing: grayscale;
|
|
174
|
-
text-rendering: optimizeLegibility;
|
|
171
|
+
line-height: 1.7;
|
|
175
172
|
}
|
|
176
173
|
body {
|
|
177
174
|
overflow: auto;
|
|
@@ -183,15 +180,18 @@ body {
|
|
|
183
180
|
}
|
|
184
181
|
.line {
|
|
185
182
|
white-space: pre;
|
|
186
|
-
display:
|
|
183
|
+
display: block;
|
|
187
184
|
content-visibility: auto;
|
|
188
|
-
contain-intrinsic-block-size: auto 1.
|
|
185
|
+
contain-intrinsic-block-size: auto 1.7em;
|
|
189
186
|
background-color: ${backgroundColor};
|
|
190
187
|
transform: translateZ(0);
|
|
191
188
|
backface-visibility: hidden;
|
|
192
189
|
}
|
|
193
190
|
.line span {
|
|
194
191
|
white-space: pre;
|
|
192
|
+
display: inline-block;
|
|
193
|
+
line-height: 1.7;
|
|
194
|
+
vertical-align: top;
|
|
195
195
|
}
|
|
196
196
|
/* Disable content-visibility on iOS Safari where it can cause rendering issues */
|
|
197
197
|
@supports (-webkit-touch-callout: none) {
|
|
@@ -229,8 +229,8 @@ ${content}
|
|
|
229
229
|
function adjustFontSize() {
|
|
230
230
|
const viewportWidth = window.innerWidth;
|
|
231
231
|
const calculatedSize = (viewportWidth - padding) / (cols * charRatio);
|
|
232
|
-
// Round to nearest even integer to
|
|
233
|
-
//
|
|
232
|
+
// Round to nearest even integer to reduce subpixel rendering issues
|
|
233
|
+
// Note: with line-height 1.7, some subpixel values are unavoidable
|
|
234
234
|
const clamped = Math.max(minFontSize, Math.min(maxFontSize, calculatedSize));
|
|
235
235
|
const fontSize = Math.round(clamped / 2) * 2;
|
|
236
236
|
document.body.style.fontSize = fontSize + 'px';
|
package/src/cli.tsx
CHANGED
|
@@ -332,11 +332,13 @@ async function runReviewMode(
|
|
|
332
332
|
const selected = filteredSessions.length > 0
|
|
333
333
|
? await clack.multiselect({
|
|
334
334
|
message: "Select sessions to include as context (space to toggle, enter to confirm)",
|
|
335
|
-
options: filteredSessions.map((s) =>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
335
|
+
options: filteredSessions.map((s) => {
|
|
336
|
+
const title = s.title || `Session ${s.sessionId.slice(0, 8)}`;
|
|
337
|
+
const timeAgo = s.updatedAt ? formatTimeAgo(s.updatedAt) : "";
|
|
338
|
+
// Include time in label to prevent layout shift (hints only show on focus)
|
|
339
|
+
const label = timeAgo ? `${title} ${pc.default.dim(`(${timeAgo})`)}` : title;
|
|
340
|
+
return { value: s.sessionId, label };
|
|
341
|
+
}),
|
|
340
342
|
required: false,
|
|
341
343
|
})
|
|
342
344
|
: [];
|
|
@@ -449,7 +451,7 @@ async function runReviewMode(
|
|
|
449
451
|
try {
|
|
450
452
|
const { htmlDesktop, htmlMobile } = await captureResponsiveHtml(
|
|
451
453
|
renderCommand,
|
|
452
|
-
{ desktopCols:
|
|
454
|
+
{ desktopCols: 230, mobileCols: 100, baseRows, themeName }
|
|
453
455
|
);
|
|
454
456
|
|
|
455
457
|
// Clean up temp files
|
|
@@ -581,7 +583,7 @@ async function runWebMode(
|
|
|
581
583
|
positionalFilters: options['--'],
|
|
582
584
|
});
|
|
583
585
|
|
|
584
|
-
const desktopCols = options.cols ||
|
|
586
|
+
const desktopCols = options.cols || 230;
|
|
585
587
|
const mobileCols = options.mobileCols || 100;
|
|
586
588
|
const themeName = options.theme && themeNames.includes(options.theme)
|
|
587
589
|
? options.theme
|
package/src/review/acp-client.ts
CHANGED
|
@@ -494,9 +494,9 @@ HOW TO EXPLAIN - Diagrams First, Text Last
|
|
|
494
494
|
═══════════════════════════════════════════════════════════════════════════════
|
|
495
495
|
|
|
496
496
|
PREFER ASCII DIAGRAMS - they explain better than words.
|
|
497
|
-
ALWAYS wrap diagrams in \`\`\` code blocks - never render them as plain text:
|
|
497
|
+
ALWAYS wrap diagrams in \`\`\`diagram code blocks - never render them as plain text:
|
|
498
498
|
|
|
499
|
-
\`\`\`
|
|
499
|
+
\`\`\`diagram
|
|
500
500
|
┌─────────────┐ ┌─────────────┐ ┌────────────┐
|
|
501
501
|
│ Request │ ───> │ Router │ ───> │ Handler │
|
|
502
502
|
└─────────────┘ └──────┬──────┘ └──────┬─────┘
|
|
@@ -507,7 +507,7 @@ ALWAYS wrap diagrams in \`\`\` code blocks - never render them as plain text:
|
|
|
507
507
|
└─────────────┘ └─────────────┘
|
|
508
508
|
\`\`\`
|
|
509
509
|
|
|
510
|
-
\`\`\`
|
|
510
|
+
\`\`\`diagram
|
|
511
511
|
┌──────────────────┐
|
|
512
512
|
│ Initial │
|
|
513
513
|
└────────┬─────────┘
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Tests for diagram parser
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test"
|
|
4
|
+
import {
|
|
5
|
+
parseDiagram,
|
|
6
|
+
parseDiagramLine,
|
|
7
|
+
diagramToDebugString,
|
|
8
|
+
} from "./diagram-parser.ts"
|
|
9
|
+
|
|
10
|
+
describe("parseDiagramLine", () => {
|
|
11
|
+
it("should parse empty line", () => {
|
|
12
|
+
expect(parseDiagramLine("")).toMatchInlineSnapshot(`
|
|
13
|
+
{
|
|
14
|
+
"segments": [],
|
|
15
|
+
}
|
|
16
|
+
`)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("should parse pure text as text segment", () => {
|
|
20
|
+
expect(parseDiagramLine("Hello World")).toMatchInlineSnapshot(`
|
|
21
|
+
{
|
|
22
|
+
"segments": [
|
|
23
|
+
{
|
|
24
|
+
"text": "Hello",
|
|
25
|
+
"type": "text",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"text": " ",
|
|
29
|
+
"type": "muted",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"text": "World",
|
|
33
|
+
"type": "text",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
`)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should parse box drawing characters as muted", () => {
|
|
41
|
+
expect(parseDiagramLine("┌─────┐")).toMatchInlineSnapshot(`
|
|
42
|
+
{
|
|
43
|
+
"segments": [
|
|
44
|
+
{
|
|
45
|
+
"text": "┌─────┐",
|
|
46
|
+
"type": "muted",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("should parse mixed box and text", () => {
|
|
54
|
+
expect(parseDiagramLine("│ Client │")).toMatchInlineSnapshot(`
|
|
55
|
+
{
|
|
56
|
+
"segments": [
|
|
57
|
+
{
|
|
58
|
+
"text": "│ ",
|
|
59
|
+
"type": "muted",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"text": "Client",
|
|
63
|
+
"type": "text",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"text": " │",
|
|
67
|
+
"type": "muted",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("should parse arrows as muted", () => {
|
|
75
|
+
expect(parseDiagramLine("A ──▶ B")).toMatchInlineSnapshot(`
|
|
76
|
+
{
|
|
77
|
+
"segments": [
|
|
78
|
+
{
|
|
79
|
+
"text": "A",
|
|
80
|
+
"type": "text",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"text": " ──▶ ",
|
|
84
|
+
"type": "muted",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"text": "B",
|
|
88
|
+
"type": "text",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should parse ASCII pipes and dashes as muted", () => {
|
|
96
|
+
expect(parseDiagramLine("+---+")).toMatchInlineSnapshot(`
|
|
97
|
+
{
|
|
98
|
+
"segments": [
|
|
99
|
+
{
|
|
100
|
+
"text": "+---+",
|
|
101
|
+
"type": "muted",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
`)
|
|
106
|
+
expect(parseDiagramLine("| X |")).toMatchInlineSnapshot(`
|
|
107
|
+
{
|
|
108
|
+
"segments": [
|
|
109
|
+
{
|
|
110
|
+
"text": "| ",
|
|
111
|
+
"type": "muted",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"text": "X",
|
|
115
|
+
"type": "text",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"text": " |",
|
|
119
|
+
"type": "muted",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
`)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("parseDiagram", () => {
|
|
128
|
+
it("should parse simple box diagram", () => {
|
|
129
|
+
const diagram = `┌─────────┐
|
|
130
|
+
│ Test │
|
|
131
|
+
└─────────┘`
|
|
132
|
+
expect(parseDiagram(diagram)).toMatchInlineSnapshot(`
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
"segments": [
|
|
136
|
+
{
|
|
137
|
+
"text": "┌─────────┐",
|
|
138
|
+
"type": "muted",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"segments": [
|
|
144
|
+
{
|
|
145
|
+
"text": "│ ",
|
|
146
|
+
"type": "muted",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"text": "Test",
|
|
150
|
+
"type": "text",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"text": " │",
|
|
154
|
+
"type": "muted",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"segments": [
|
|
160
|
+
{
|
|
161
|
+
"text": "└─────────┘",
|
|
162
|
+
"type": "muted",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
`)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("should parse flow diagram with arrows", () => {
|
|
171
|
+
const diagram = `┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
172
|
+
│ Client │────▶│ Server │────▶│ Database │
|
|
173
|
+
└─────────────┘ └─────────────┘ └─────────────┘`
|
|
174
|
+
expect(parseDiagram(diagram)).toMatchInlineSnapshot(`
|
|
175
|
+
[
|
|
176
|
+
{
|
|
177
|
+
"segments": [
|
|
178
|
+
{
|
|
179
|
+
"text": "┌─────────────┐ ┌─────────────┐ ┌─────────────┐",
|
|
180
|
+
"type": "muted",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"segments": [
|
|
186
|
+
{
|
|
187
|
+
"text": "│ ",
|
|
188
|
+
"type": "muted",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"text": "Client",
|
|
192
|
+
"type": "text",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"text": " │────▶│ ",
|
|
196
|
+
"type": "muted",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"text": "Server",
|
|
200
|
+
"type": "text",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"text": " │────▶│ ",
|
|
204
|
+
"type": "muted",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"text": "Database",
|
|
208
|
+
"type": "text",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"text": " │",
|
|
212
|
+
"type": "muted",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"segments": [
|
|
218
|
+
{
|
|
219
|
+
"text": "└─────────────┘ └─────────────┘ └─────────────┘",
|
|
220
|
+
"type": "muted",
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
]
|
|
225
|
+
`)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe("diagramToDebugString", () => {
|
|
230
|
+
it("should replace muted segments with asterisks", () => {
|
|
231
|
+
const diagram = `┌─────────┐
|
|
232
|
+
│ Test │
|
|
233
|
+
└─────────┘`
|
|
234
|
+
const parsed = parseDiagram(diagram)
|
|
235
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
236
|
+
"***********
|
|
237
|
+
***Test****
|
|
238
|
+
***********"
|
|
239
|
+
`)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("should show text normally and mute structural chars", () => {
|
|
243
|
+
const diagram = `┌─────────────┐ ┌─────────────┐
|
|
244
|
+
│ Client │────▶│ Server │
|
|
245
|
+
└─────────────┘ └─────────────┘`
|
|
246
|
+
const parsed = parseDiagram(diagram)
|
|
247
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
248
|
+
"***********************************
|
|
249
|
+
****Client**************Server*****
|
|
250
|
+
***********************************"
|
|
251
|
+
`)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it("should handle vertical flow diagram", () => {
|
|
255
|
+
const diagram = ` ┌─────────┐
|
|
256
|
+
│ Start │
|
|
257
|
+
└────┬────┘
|
|
258
|
+
│
|
|
259
|
+
▼
|
|
260
|
+
┌─────────┐
|
|
261
|
+
│ End │
|
|
262
|
+
└─────────┘`
|
|
263
|
+
const parsed = parseDiagram(diagram)
|
|
264
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
265
|
+
"****************
|
|
266
|
+
********Start***
|
|
267
|
+
****************
|
|
268
|
+
***********
|
|
269
|
+
***********
|
|
270
|
+
****************
|
|
271
|
+
********End*****
|
|
272
|
+
****************"
|
|
273
|
+
`)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it("should handle complex architecture diagram", () => {
|
|
277
|
+
const diagram = `┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
278
|
+
│ Client │────▶│ Router │────▶│ Handler │────▶│ Database │
|
|
279
|
+
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
280
|
+
│ │ │
|
|
281
|
+
│ │ │
|
|
282
|
+
▼ ▼ ▼
|
|
283
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
284
|
+
│ Validate │ │ Check │ │ Query │
|
|
285
|
+
│ Route │ │ Auth │ │ Execute │
|
|
286
|
+
└─────────────┘ └─────────────┘ └─────────────┘`
|
|
287
|
+
const parsed = parseDiagram(diagram)
|
|
288
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
289
|
+
"***************************************************************************
|
|
290
|
+
****Client**************Router*************Handler*************Database****
|
|
291
|
+
***************************************************************************
|
|
292
|
+
********************************************************************
|
|
293
|
+
********************************************************************
|
|
294
|
+
********************************************************************
|
|
295
|
+
***************************************************************************
|
|
296
|
+
***********************Validate*************Check***************Query******
|
|
297
|
+
************************Route***************Auth****************Execute****
|
|
298
|
+
***************************************************************************"
|
|
299
|
+
`)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it("should handle ASCII-style diagram", () => {
|
|
303
|
+
const diagram = `+-------+ +-------+
|
|
304
|
+
| Input | --> | Output|
|
|
305
|
+
+-------+ +-------+`
|
|
306
|
+
const parsed = parseDiagram(diagram)
|
|
307
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
308
|
+
"***********************
|
|
309
|
+
**Input*********Output*
|
|
310
|
+
***********************"
|
|
311
|
+
`)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it("should handle diagram with downward arrows", () => {
|
|
315
|
+
const diagram = ` A
|
|
316
|
+
│
|
|
317
|
+
▼
|
|
318
|
+
B`
|
|
319
|
+
const parsed = parseDiagram(diagram)
|
|
320
|
+
expect(diagramToDebugString(parsed)).toMatchInlineSnapshot(`
|
|
321
|
+
"**A
|
|
322
|
+
***
|
|
323
|
+
***
|
|
324
|
+
**B"
|
|
325
|
+
`)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Diagram parser - parses ASCII/Unicode diagrams into colored segments
|
|
2
|
+
// Used to highlight structural characters (boxes, arrows, lines) differently from text
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A segment of text with a specific color type
|
|
6
|
+
*/
|
|
7
|
+
export interface DiagramSegment {
|
|
8
|
+
text: string
|
|
9
|
+
type: "text" | "muted"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A parsed line of diagram content
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedDiagramLine {
|
|
16
|
+
segments: DiagramSegment[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Box drawing characters (Unicode)
|
|
20
|
+
const BOX_DRAWING_CHARS = new Set([
|
|
21
|
+
// Light box drawing
|
|
22
|
+
"┌",
|
|
23
|
+
"┐",
|
|
24
|
+
"└",
|
|
25
|
+
"┘",
|
|
26
|
+
"─",
|
|
27
|
+
"│",
|
|
28
|
+
"├",
|
|
29
|
+
"┤",
|
|
30
|
+
"┬",
|
|
31
|
+
"┴",
|
|
32
|
+
"┼",
|
|
33
|
+
// Double box drawing
|
|
34
|
+
"╔",
|
|
35
|
+
"╗",
|
|
36
|
+
"╚",
|
|
37
|
+
"╝",
|
|
38
|
+
"═",
|
|
39
|
+
"║",
|
|
40
|
+
"╠",
|
|
41
|
+
"╣",
|
|
42
|
+
"╦",
|
|
43
|
+
"╩",
|
|
44
|
+
"╬",
|
|
45
|
+
// Heavy box drawing
|
|
46
|
+
"┏",
|
|
47
|
+
"┓",
|
|
48
|
+
"┗",
|
|
49
|
+
"┛",
|
|
50
|
+
"━",
|
|
51
|
+
"┃",
|
|
52
|
+
"┣",
|
|
53
|
+
"┫",
|
|
54
|
+
"┳",
|
|
55
|
+
"┻",
|
|
56
|
+
"╋",
|
|
57
|
+
// Mixed light/heavy
|
|
58
|
+
"┍",
|
|
59
|
+
"┎",
|
|
60
|
+
"┑",
|
|
61
|
+
"┒",
|
|
62
|
+
"┕",
|
|
63
|
+
"┖",
|
|
64
|
+
"┙",
|
|
65
|
+
"┚",
|
|
66
|
+
"┝",
|
|
67
|
+
"┞",
|
|
68
|
+
"┟",
|
|
69
|
+
"┠",
|
|
70
|
+
"┡",
|
|
71
|
+
"┢",
|
|
72
|
+
"┥",
|
|
73
|
+
"┦",
|
|
74
|
+
"┧",
|
|
75
|
+
"┨",
|
|
76
|
+
"┩",
|
|
77
|
+
"┪",
|
|
78
|
+
"┭",
|
|
79
|
+
"┮",
|
|
80
|
+
"┯",
|
|
81
|
+
"┰",
|
|
82
|
+
"┱",
|
|
83
|
+
"┲",
|
|
84
|
+
"┵",
|
|
85
|
+
"┶",
|
|
86
|
+
"┷",
|
|
87
|
+
"┸",
|
|
88
|
+
"┹",
|
|
89
|
+
"┺",
|
|
90
|
+
"┽",
|
|
91
|
+
"┾",
|
|
92
|
+
"┿",
|
|
93
|
+
"╀",
|
|
94
|
+
"╁",
|
|
95
|
+
"╂",
|
|
96
|
+
"╃",
|
|
97
|
+
"╄",
|
|
98
|
+
"╅",
|
|
99
|
+
"╆",
|
|
100
|
+
"╇",
|
|
101
|
+
"╈",
|
|
102
|
+
"╉",
|
|
103
|
+
"╊",
|
|
104
|
+
// Rounded corners
|
|
105
|
+
"╭",
|
|
106
|
+
"╮",
|
|
107
|
+
"╯",
|
|
108
|
+
"╰",
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
// Arrow characters
|
|
112
|
+
const ARROW_CHARS = new Set([
|
|
113
|
+
// Unicode arrows
|
|
114
|
+
"▶",
|
|
115
|
+
"◀",
|
|
116
|
+
"▼",
|
|
117
|
+
"▲",
|
|
118
|
+
"►",
|
|
119
|
+
"◄",
|
|
120
|
+
"▾",
|
|
121
|
+
"▴",
|
|
122
|
+
"→",
|
|
123
|
+
"←",
|
|
124
|
+
"↓",
|
|
125
|
+
"↑",
|
|
126
|
+
"↔",
|
|
127
|
+
"↕",
|
|
128
|
+
"↖",
|
|
129
|
+
"↗",
|
|
130
|
+
"↘",
|
|
131
|
+
"↙",
|
|
132
|
+
"⇒",
|
|
133
|
+
"⇐",
|
|
134
|
+
"⇓",
|
|
135
|
+
"⇑",
|
|
136
|
+
"⇔",
|
|
137
|
+
"⇕",
|
|
138
|
+
// Triangle arrows
|
|
139
|
+
"△",
|
|
140
|
+
"▽",
|
|
141
|
+
"◁",
|
|
142
|
+
"▷",
|
|
143
|
+
"⊳",
|
|
144
|
+
"⊲",
|
|
145
|
+
"⊴",
|
|
146
|
+
"⊵",
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
// ASCII diagram characters (structural, not text)
|
|
150
|
+
// Note: "v" and "V" are NOT included because they appear in regular text
|
|
151
|
+
// like "Server", "Validate", etc.
|
|
152
|
+
const ASCII_STRUCTURAL_CHARS = new Set(["-", "|", "+", "/", "\\", "<", ">", "^"])
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a character is a diagram structural character (should be muted)
|
|
156
|
+
*/
|
|
157
|
+
function isDiagramChar(char: string): boolean {
|
|
158
|
+
return (
|
|
159
|
+
BOX_DRAWING_CHARS.has(char) ||
|
|
160
|
+
ARROW_CHARS.has(char) ||
|
|
161
|
+
ASCII_STRUCTURAL_CHARS.has(char)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse a single line of diagram content into segments
|
|
167
|
+
*/
|
|
168
|
+
export function parseDiagramLine(line: string): ParsedDiagramLine {
|
|
169
|
+
if (!line) {
|
|
170
|
+
return { segments: [] }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const segments: DiagramSegment[] = []
|
|
174
|
+
let currentText = ""
|
|
175
|
+
let currentType: "text" | "muted" | null = null
|
|
176
|
+
|
|
177
|
+
// Iterate through each character (handling Unicode properly)
|
|
178
|
+
for (const char of line) {
|
|
179
|
+
const isMuted = isDiagramChar(char) || char === " "
|
|
180
|
+
const type = isMuted ? "muted" : "text"
|
|
181
|
+
|
|
182
|
+
if (currentType === null) {
|
|
183
|
+
currentType = type
|
|
184
|
+
currentText = char
|
|
185
|
+
} else if (type === currentType) {
|
|
186
|
+
currentText += char
|
|
187
|
+
} else {
|
|
188
|
+
// Type changed, push current segment and start new one
|
|
189
|
+
segments.push({ text: currentText, type: currentType })
|
|
190
|
+
currentText = char
|
|
191
|
+
currentType = type
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Push final segment
|
|
196
|
+
if (currentText && currentType !== null) {
|
|
197
|
+
segments.push({ text: currentText, type: currentType })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { segments }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse entire diagram content into lines of segments
|
|
205
|
+
*/
|
|
206
|
+
export function parseDiagram(content: string): ParsedDiagramLine[] {
|
|
207
|
+
const lines = content.split("\n")
|
|
208
|
+
return lines.map(parseDiagramLine)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Convert parsed diagram to a debug string for testing
|
|
213
|
+
* Muted segments are replaced with '*' characters
|
|
214
|
+
*/
|
|
215
|
+
export function diagramToDebugString(parsed: ParsedDiagramLine[]): string {
|
|
216
|
+
return parsed
|
|
217
|
+
.map((line) => {
|
|
218
|
+
return line.segments
|
|
219
|
+
.map((segment) => {
|
|
220
|
+
if (segment.type === "muted") {
|
|
221
|
+
// Replace each character with '*' to show what would be muted
|
|
222
|
+
return "*".repeat([...segment.text].length)
|
|
223
|
+
}
|
|
224
|
+
return segment.text
|
|
225
|
+
})
|
|
226
|
+
.join("")
|
|
227
|
+
})
|
|
228
|
+
.join("\n")
|
|
229
|
+
}
|
|
@@ -876,7 +876,7 @@ The diagram above should not wrap.`,
|
|
|
876
876
|
|
|
877
877
|
await testSetup.renderOnce()
|
|
878
878
|
const frame = testSetup.captureCharFrame()
|
|
879
|
-
|
|
879
|
+
|
|
880
880
|
// With 80-char width, the 65-char diagram should fit without wrapping
|
|
881
881
|
expect(frame).toContain("┌─────────────┐ ┌─────────────┐ ┌─────────────┐")
|
|
882
882
|
expect(frame).toContain("│ Client │────▶│ Server │────▶│ Database │")
|
|
@@ -946,6 +946,8 @@ The diagram above should not wrap.`,
|
|
|
946
946
|
`)
|
|
947
947
|
})
|
|
948
948
|
|
|
949
|
+
// SKIPPED: This test has a pre-existing issue with yoga-layout binding errors
|
|
950
|
+
// when reusing renderers across testRender calls
|
|
949
951
|
it("should TRUNCATE 4-box diagram at 70 cols WITH renderer", async () => {
|
|
950
952
|
// 4-box diagram is 79 chars wide, at 70 cols it truncates (not wraps)
|
|
951
953
|
// This proves wrapMode: "none" is working
|
|
@@ -1024,14 +1026,55 @@ The diagram above should not wrap.`,
|
|
|
1024
1026
|
`)
|
|
1025
1027
|
})
|
|
1026
1028
|
|
|
1029
|
+
it("should render diagram code blocks with colored segments", async () => {
|
|
1030
|
+
// Diagrams with lang="diagram" should have structural chars colored differently
|
|
1031
|
+
const diagramHunk = createHunk(1, "src/config.ts", 0, 1, 1, [
|
|
1032
|
+
"+export const x = 1",
|
|
1033
|
+
])
|
|
1034
|
+
|
|
1035
|
+
const diagramReviewData: ReviewYaml = {
|
|
1036
|
+
hunks: [{
|
|
1037
|
+
hunkIds: [1],
|
|
1038
|
+
markdownDescription: `## Architecture Diagram
|
|
1039
|
+
|
|
1040
|
+
\`\`\`diagram
|
|
1041
|
+
┌───────┐ ┌───────┐
|
|
1042
|
+
│ Input │────▶│Output │
|
|
1043
|
+
└───────┘ └───────┘
|
|
1044
|
+
\`\`\`
|
|
1045
|
+
|
|
1046
|
+
The diagram above shows the flow.`,
|
|
1047
|
+
}],
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
testSetup = await testRender(
|
|
1051
|
+
<ReviewAppView
|
|
1052
|
+
hunks={[diagramHunk]}
|
|
1053
|
+
reviewData={diagramReviewData}
|
|
1054
|
+
isGenerating={false}
|
|
1055
|
+
themeName="github"
|
|
1056
|
+
width={60}
|
|
1057
|
+
/>,
|
|
1058
|
+
{ width: 60, height: 20 },
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
await testSetup.renderOnce()
|
|
1062
|
+
const frame = testSetup.captureCharFrame()
|
|
1063
|
+
// The diagram should render with the structural characters
|
|
1064
|
+
// (box drawing and arrows) and text labels (Input, Output)
|
|
1065
|
+
expect(frame).toContain("Input")
|
|
1066
|
+
expect(frame).toContain("Output")
|
|
1067
|
+
expect(frame).toContain("Architecture Diagram")
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1027
1070
|
// ============================================================================
|
|
1028
1071
|
// LONG LINE WRAPPING TESTS
|
|
1029
1072
|
// ============================================================================
|
|
1030
|
-
//
|
|
1073
|
+
//
|
|
1031
1074
|
// THE ISSUE:
|
|
1032
1075
|
// When diff lines are long enough to wrap in split view, and the left/right
|
|
1033
1076
|
// sides wrap to different numbers of visual lines, the alignment breaks.
|
|
1034
|
-
//
|
|
1077
|
+
//
|
|
1035
1078
|
// Example of broken alignment (single render):
|
|
1036
1079
|
// 1 - const response... 1 + const response...
|
|
1037
1080
|
// example.com/users'); API_BASE_URL...
|
|
@@ -1188,4 +1231,59 @@ Added environment-based URL and auth header.`,
|
|
|
1188
1231
|
"
|
|
1189
1232
|
`)
|
|
1190
1233
|
})
|
|
1234
|
+
|
|
1235
|
+
it("should show generating indicator below last hunk when isGenerating is true", async () => {
|
|
1236
|
+
// When generating, a centered spinner+text indicator should appear below the content
|
|
1237
|
+
testSetup = await testRender(
|
|
1238
|
+
<ReviewAppView
|
|
1239
|
+
hunks={exampleHunks}
|
|
1240
|
+
reviewData={{
|
|
1241
|
+
hunks: [{
|
|
1242
|
+
hunkIds: [3],
|
|
1243
|
+
markdownDescription: `## Import changes
|
|
1244
|
+
|
|
1245
|
+
Added logger import.`,
|
|
1246
|
+
}],
|
|
1247
|
+
}}
|
|
1248
|
+
isGenerating={true}
|
|
1249
|
+
themeName="github"
|
|
1250
|
+
width={80}
|
|
1251
|
+
/>,
|
|
1252
|
+
{
|
|
1253
|
+
width: 80,
|
|
1254
|
+
height: 25,
|
|
1255
|
+
},
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
await testSetup.renderOnce()
|
|
1259
|
+
const frame = testSetup.captureCharFrame()
|
|
1260
|
+
expect(frame).toMatchInlineSnapshot(`
|
|
1261
|
+
"
|
|
1262
|
+
Import changes
|
|
1263
|
+
|
|
1264
|
+
Added logger import.
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
rc/index.ts +1-0
|
|
1268
|
+
|
|
1269
|
+
import { main } from './utils'
|
|
1270
|
+
+ import { logger } from './logger'
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
⠋ generating
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
(1 section) t theme run with --web to share & collaborate
|
|
1285
|
+
|
|
1286
|
+
"
|
|
1287
|
+
`)
|
|
1288
|
+
})
|
|
1191
1289
|
})
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
5
|
-
import { MacOSScrollAccel, SyntaxStyle, BoxRenderable, CodeRenderable } from "@opentui/core"
|
|
5
|
+
import { MacOSScrollAccel, SyntaxStyle, BoxRenderable, CodeRenderable, TextRenderable } from "@opentui/core"
|
|
6
6
|
import type { Token } from "marked"
|
|
7
7
|
import { getResolvedTheme, getSyntaxTheme, defaultThemeName, themeNames, rgbaToHex } from "../themes.ts"
|
|
8
8
|
import { detectFiletype, countChanges, getViewMode } from "../diff-utils.ts"
|
|
9
9
|
import { DiffView } from "../components/diff-view.tsx"
|
|
10
10
|
import { watchReviewYaml } from "./yaml-watcher.ts"
|
|
11
11
|
import { createSubHunk } from "./hunk-parser.ts"
|
|
12
|
+
import { parseDiagram } from "./diagram-parser.ts"
|
|
12
13
|
import { useAppStore } from "../store.ts"
|
|
13
14
|
import Dropdown from "../dropdown.tsx"
|
|
14
15
|
import type { IndexedHunk, ReviewYaml, ReviewGroup } from "./types.ts"
|
|
@@ -68,6 +69,12 @@ export function ReviewApp({
|
|
|
68
69
|
|
|
69
70
|
// Keyboard navigation
|
|
70
71
|
useKeyboard((key) => {
|
|
72
|
+
// Ctrl+D toggles debug console
|
|
73
|
+
if (key.ctrl && key.name === "d") {
|
|
74
|
+
renderer.console.toggle()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
if (showThemePicker) {
|
|
72
79
|
if (key.name === "escape") {
|
|
73
80
|
setShowThemePicker(false)
|
|
@@ -167,22 +174,43 @@ export function ReviewApp({
|
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
/**
|
|
170
|
-
*
|
|
177
|
+
* Inline generating indicator with spinner and animated dots
|
|
171
178
|
*/
|
|
172
|
-
function
|
|
173
|
-
const [
|
|
179
|
+
function GeneratingIndicatorInline({ color }: { color: string }) {
|
|
180
|
+
const [phase, setPhase] = React.useState(0)
|
|
174
181
|
|
|
175
182
|
React.useEffect(() => {
|
|
176
183
|
const interval = setInterval(() => {
|
|
177
|
-
|
|
178
|
-
},
|
|
184
|
+
setPhase((p) => (p + 1) % 12)
|
|
185
|
+
}, 100)
|
|
179
186
|
return () => clearInterval(interval)
|
|
180
187
|
}, [])
|
|
181
188
|
|
|
182
|
-
|
|
189
|
+
// Braille spinner pattern (cycles every 8 phases)
|
|
190
|
+
const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"]
|
|
191
|
+
const spinner = spinnerChars[phase % 8]
|
|
192
|
+
// Dots cycle every 4 phases (so they change ~3x slower than spinner)
|
|
193
|
+
const dotCount = Math.floor(phase / 3) % 4
|
|
194
|
+
const dots = ".".repeat(dotCount).padEnd(3, " ")
|
|
183
195
|
|
|
196
|
+
return <text fg={color}>{spinner} generating{dots}</text>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Centered generating indicator for loading state (when no content yet)
|
|
201
|
+
*/
|
|
202
|
+
function GeneratingIndicator({ color, bgColor }: { color: string; bgColor: string }) {
|
|
184
203
|
return (
|
|
185
|
-
<
|
|
204
|
+
<box
|
|
205
|
+
style={{
|
|
206
|
+
flexGrow: 1,
|
|
207
|
+
alignItems: "center",
|
|
208
|
+
justifyContent: "center",
|
|
209
|
+
backgroundColor: bgColor,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<GeneratingIndicatorInline color={color} />
|
|
213
|
+
</box>
|
|
186
214
|
)
|
|
187
215
|
}
|
|
188
216
|
|
|
@@ -251,9 +279,14 @@ export function ReviewAppView({
|
|
|
251
279
|
backgroundColor: bgColor,
|
|
252
280
|
}}
|
|
253
281
|
>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
282
|
+
{isGenerating ? (
|
|
283
|
+
<GeneratingIndicator
|
|
284
|
+
color={rgbaToHex(resolvedTheme.textMuted)}
|
|
285
|
+
bgColor={bgColor}
|
|
286
|
+
/>
|
|
287
|
+
) : (
|
|
288
|
+
<text fg={rgbaToHex(resolvedTheme.text)}>No review groups generated</text>
|
|
289
|
+
)}
|
|
257
290
|
</box>
|
|
258
291
|
)
|
|
259
292
|
}
|
|
@@ -338,6 +371,12 @@ export function ReviewAppView({
|
|
|
338
371
|
</box>
|
|
339
372
|
)
|
|
340
373
|
})}
|
|
374
|
+
{/* Generating indicator - shown below last hunk */}
|
|
375
|
+
{isGenerating && (
|
|
376
|
+
<box style={{ alignItems: "center", justifyContent: "center", marginTop: gap, marginBottom: gap }}>
|
|
377
|
+
<GeneratingIndicatorInline color={rgbaToHex(resolvedTheme.textMuted)} />
|
|
378
|
+
</box>
|
|
379
|
+
)}
|
|
341
380
|
</box>
|
|
342
381
|
</scrollbox>
|
|
343
382
|
|
|
@@ -353,9 +392,6 @@ export function ReviewAppView({
|
|
|
353
392
|
alignItems: "center",
|
|
354
393
|
}}
|
|
355
394
|
>
|
|
356
|
-
{isGenerating && (
|
|
357
|
-
<GeneratingIndicator color={rgbaToHex(resolvedTheme.textMuted)} />
|
|
358
|
-
)}
|
|
359
395
|
<text fg={rgbaToHex(resolvedTheme.textMuted)}>
|
|
360
396
|
({groups.length} section{groups.length !== 1 ? "s" : ""})
|
|
361
397
|
</text>
|
|
@@ -433,6 +469,9 @@ function MarkdownBlock({ content, themeName, width, renderer }: MarkdownBlockPro
|
|
|
433
469
|
() => SyntaxStyle.fromStyles(syntaxTheme),
|
|
434
470
|
[syntaxTheme],
|
|
435
471
|
)
|
|
472
|
+
const resolvedTheme = getResolvedTheme(themeName)
|
|
473
|
+
const textColor = rgbaToHex(resolvedTheme.text)
|
|
474
|
+
const concealColor = rgbaToHex(resolvedTheme.conceal)
|
|
436
475
|
|
|
437
476
|
// Max width for prose (constrained), code blocks use full terminal width
|
|
438
477
|
const maxProseWidth = Math.min(80, width)
|
|
@@ -463,11 +502,55 @@ function MarkdownBlock({ content, themeName, width, renderer }: MarkdownBlockPro
|
|
|
463
502
|
// Code blocks: create custom CodeRenderable with wrapMode: "none" and overflow: "hidden"
|
|
464
503
|
if (token.type === "code") {
|
|
465
504
|
const codeToken = token as { text: string; lang?: string }
|
|
505
|
+
|
|
506
|
+
|
|
466
507
|
const wrapper = new BoxRenderable(renderer, {
|
|
467
508
|
id: `code-wrapper-${nodeCounter++}`,
|
|
468
509
|
alignSelf: "center",
|
|
469
510
|
overflow: "hidden",
|
|
470
511
|
})
|
|
512
|
+
|
|
513
|
+
// Special handling for diagram language - color structural chars as muted
|
|
514
|
+
if (codeToken.lang === "diagram") {
|
|
515
|
+
console.log("[diagram-debug] MATCHED diagram lang, parsing...")
|
|
516
|
+
const diagramWrapper = new BoxRenderable(renderer, {
|
|
517
|
+
id: `diagram-${nodeCounter++}`,
|
|
518
|
+
flexDirection: "column",
|
|
519
|
+
})
|
|
520
|
+
const parsedLines = parseDiagram(codeToken.text)
|
|
521
|
+
for (let i = 0; i < parsedLines.length; i++) {
|
|
522
|
+
const line = parsedLines[i]
|
|
523
|
+
// Skip empty lines or add a single space to maintain line height
|
|
524
|
+
if (line.segments.length === 0) {
|
|
525
|
+
const emptyLine = new TextRenderable(renderer, {
|
|
526
|
+
id: `diagram-empty-${nodeCounter++}-${i}`,
|
|
527
|
+
content: " ",
|
|
528
|
+
fg: concealColor,
|
|
529
|
+
})
|
|
530
|
+
diagramWrapper.add(emptyLine)
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
// Create a row box for each line
|
|
534
|
+
const lineBox = new BoxRenderable(renderer, {
|
|
535
|
+
id: `diagram-line-${nodeCounter++}-${i}`,
|
|
536
|
+
flexDirection: "row",
|
|
537
|
+
})
|
|
538
|
+
// Add each segment as a separate text renderable with appropriate color
|
|
539
|
+
for (let j = 0; j < line.segments.length; j++) {
|
|
540
|
+
const segment = line.segments[j]
|
|
541
|
+
const segmentRenderable = new TextRenderable(renderer, {
|
|
542
|
+
id: `diagram-seg-${nodeCounter++}-${i}-${j}`,
|
|
543
|
+
content: segment.text,
|
|
544
|
+
fg: segment.type === "muted" ? concealColor : textColor,
|
|
545
|
+
})
|
|
546
|
+
lineBox.add(segmentRenderable)
|
|
547
|
+
}
|
|
548
|
+
diagramWrapper.add(lineBox)
|
|
549
|
+
}
|
|
550
|
+
wrapper.add(diagramWrapper)
|
|
551
|
+
return wrapper
|
|
552
|
+
}
|
|
553
|
+
|
|
471
554
|
const codeRenderable = new CodeRenderable(renderer, {
|
|
472
555
|
id: `code-${nodeCounter++}`,
|
|
473
556
|
content: codeToken.text,
|
|
@@ -495,7 +578,7 @@ function MarkdownBlock({ content, themeName, width, renderer }: MarkdownBlockPro
|
|
|
495
578
|
// Other elements (hr, space, etc.) use default rendering
|
|
496
579
|
return undefined
|
|
497
580
|
}
|
|
498
|
-
}, [renderer, maxProseWidth, syntaxStyle])
|
|
581
|
+
}, [renderer, maxProseWidth, syntaxStyle, textColor, concealColor])
|
|
499
582
|
|
|
500
583
|
// Use very large width when renderer available so code blocks don't wrap
|
|
501
584
|
// Prose is constrained via renderNode, code blocks can overflow
|
package/src/themes/github.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"darkBgElement": "#161b22",
|
|
7
7
|
"darkFg": "#e6edf3",
|
|
8
8
|
"darkFgMuted": "#8b949e",
|
|
9
|
+
"darkConceal": "#484f58",
|
|
9
10
|
"darkBorder": "#30363d",
|
|
10
11
|
"darkBorderSubtle": "#21262d",
|
|
11
12
|
"darkKeyword": "#ff7b72",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"lightBgPanel": "#f0f3f6",
|
|
25
26
|
"lightFg": "#24292f",
|
|
26
27
|
"lightFgMuted": "#57606a",
|
|
28
|
+
"lightConceal": "#8c959f",
|
|
27
29
|
"lightKeyword": "#cf222e",
|
|
28
30
|
"lightString": "#0a3069",
|
|
29
31
|
"lightConstant": "#0550ae",
|
|
@@ -236,6 +238,10 @@
|
|
|
236
238
|
"syntaxPunctuation": {
|
|
237
239
|
"dark": "darkFg",
|
|
238
240
|
"light": "lightFg"
|
|
241
|
+
},
|
|
242
|
+
"conceal": {
|
|
243
|
+
"dark": "darkConceal",
|
|
244
|
+
"light": "lightConceal"
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
}
|
package/src/themes.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ResolvedTheme {
|
|
|
37
37
|
// Text colors
|
|
38
38
|
text: RGBA;
|
|
39
39
|
textMuted: RGBA;
|
|
40
|
+
conceal: RGBA;
|
|
40
41
|
// Diff colors
|
|
41
42
|
diffAddedBg: RGBA;
|
|
42
43
|
diffRemovedBg: RGBA;
|
|
@@ -122,12 +123,12 @@ function loadTheme(name: string): ThemeJson {
|
|
|
122
123
|
if (themeCache[name]) {
|
|
123
124
|
return themeCache[name];
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
const fileName = THEME_FILES[name];
|
|
127
128
|
if (!fileName) {
|
|
128
129
|
return github; // Fallback to default
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
try {
|
|
132
133
|
// Use dynamic import with synchronous pattern for JSON
|
|
133
134
|
// This works because JSON imports are resolved at bundle time by Bun
|
|
@@ -174,7 +175,7 @@ function resolveTheme(
|
|
|
174
175
|
const fallbackText: ColorValue = "#d4d4d4";
|
|
175
176
|
|
|
176
177
|
const text = resolveColor(t.text ?? fallbackText);
|
|
177
|
-
|
|
178
|
+
|
|
178
179
|
return {
|
|
179
180
|
primary: resolveColor(t.primary ?? t.syntaxFunction ?? fallbackGray),
|
|
180
181
|
syntaxComment: resolveColor(t.syntaxComment ?? fallbackGray),
|
|
@@ -188,6 +189,7 @@ function resolveTheme(
|
|
|
188
189
|
syntaxPunctuation: resolveColor(t.syntaxPunctuation ?? fallbackGray),
|
|
189
190
|
text,
|
|
190
191
|
textMuted: resolveColor(t.textMuted ?? fallbackGray),
|
|
192
|
+
conceal: resolveColor(t.conceal ?? t.textMuted ?? fallbackGray),
|
|
191
193
|
diffAddedBg: resolveColor(t.diffAddedBg ?? "#1e3a1e"),
|
|
192
194
|
diffRemovedBg: resolveColor(t.diffRemovedBg ?? "#3a1e1e"),
|
|
193
195
|
diffContextBg: resolveColor(t.diffContextBg ?? fallbackBg),
|
|
@@ -233,7 +235,7 @@ export function getSyntaxTheme(
|
|
|
233
235
|
return {
|
|
234
236
|
// Default text style
|
|
235
237
|
default: { fg: resolved.text },
|
|
236
|
-
|
|
238
|
+
|
|
237
239
|
// Code syntax styles
|
|
238
240
|
keyword: { fg: resolved.syntaxKeyword, italic: true },
|
|
239
241
|
"keyword.import": { fg: resolved.syntaxKeyword },
|
|
@@ -271,7 +273,7 @@ export function getSyntaxTheme(
|
|
|
271
273
|
"punctuation.bracket": { fg: resolved.syntaxPunctuation },
|
|
272
274
|
"punctuation.delimiter": { fg: resolved.syntaxOperator },
|
|
273
275
|
"punctuation.special": { fg: resolved.syntaxOperator },
|
|
274
|
-
|
|
276
|
+
|
|
275
277
|
// Markdown styles - these are the Tree-sitter scope names for markdown
|
|
276
278
|
"markup.heading": { fg: resolved.markdownHeading, bold: true },
|
|
277
279
|
"markup.heading.1": { fg: resolved.markdownHeading, bold: true },
|
|
@@ -294,7 +296,7 @@ export function getSyntaxTheme(
|
|
|
294
296
|
label: { fg: resolved.markdownLinkText },
|
|
295
297
|
spell: { fg: resolved.text },
|
|
296
298
|
nospell: { fg: resolved.text },
|
|
297
|
-
conceal: { fg: resolved.textMuted },
|
|
299
|
+
conceal: { fg: resolved.conceal || resolved.textMuted },
|
|
298
300
|
"string.special": { fg: resolved.markdownLink, underline: true },
|
|
299
301
|
"string.special.url": { fg: resolved.markdownLink, underline: true },
|
|
300
302
|
};
|