@effect-tui/react 0.7.0 → 0.9.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/dist/src/components/Divider.d.ts.map +1 -1
- package/dist/src/components/Divider.js +1 -1
- package/dist/src/components/Divider.js.map +1 -1
- package/dist/src/components/Markdown.js +13 -13
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +1 -1
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/Table.d.ts.map +1 -1
- package/dist/src/components/Table.js +1 -1
- package/dist/src/components/Table.js.map +1 -1
- package/dist/src/components/TextInput.d.ts.map +1 -1
- package/dist/src/components/TextInput.js +1 -1
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/console/ConsoleCapture.d.ts +25 -0
- package/dist/src/console/ConsoleCapture.d.ts.map +1 -1
- package/dist/src/console/ConsoleCapture.js +153 -9
- package/dist/src/console/ConsoleCapture.js.map +1 -1
- package/dist/src/console/ConsolePopover.js +10 -10
- package/dist/src/console/ConsolePopover.js.map +1 -1
- package/dist/src/console/useConsole.d.ts.map +1 -1
- package/dist/src/console/useConsole.js +3 -5
- package/dist/src/console/useConsole.js.map +1 -1
- package/dist/src/debug/DiagnosticsPanel.js +1 -1
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
- package/dist/src/dev/Toast.d.ts.map +1 -1
- package/dist/src/dev/Toast.js +4 -8
- package/dist/src/dev/Toast.js.map +1 -1
- package/dist/src/dev.js +1 -1
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +14 -9
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hosts/box.d.ts +6 -0
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +15 -1
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.js +1 -1
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.js +1 -1
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/inline/index.js +5 -5
- package/dist/src/inline/index.js.map +1 -1
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +25 -1
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/visualize/index.js +3 -3
- package/dist/src/visualize/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/Divider.tsx +1 -1
- package/src/components/Markdown.tsx +13 -13
- package/src/components/MultilineTextInput.tsx +5 -5
- package/src/components/Table.tsx +2 -2
- package/src/components/TextInput.tsx +4 -4
- package/src/console/ConsoleCapture.ts +174 -9
- package/src/console/ConsolePopover.tsx +12 -12
- package/src/console/useConsole.ts +3 -6
- package/src/debug/DiagnosticsPanel.tsx +4 -4
- package/src/dev/Toast.tsx +8 -9
- package/src/dev.tsx +1 -1
- package/src/hooks/use-scroll.ts +18 -9
- package/src/hosts/box.ts +21 -1
- package/src/hosts/canvas.ts +1 -1
- package/src/hosts/codeblock.ts +1 -1
- package/src/inline/index.tsx +5 -5
- package/src/renderer.ts +24 -1
- package/src/visualize/index.tsx +11 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.
|
|
86
|
+
"@effect-tui/core": "^0.9.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -17,7 +17,7 @@ export interface DividerProps {
|
|
|
17
17
|
* <Divider char="═" fg={Colors.brightBlue} />
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export function Divider({ char = "─", fg = Colors.gray(12) }: DividerProps) {
|
|
20
|
+
export function Divider({ char = "─", fg = Colors.ansi.gray(12) }: DividerProps) {
|
|
21
21
|
const { width } = useTerminalSize()
|
|
22
22
|
return <text fg={fg}>{char.repeat(width)}</text>
|
|
23
23
|
}
|
|
@@ -31,19 +31,19 @@ export interface MarkdownTheme {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const defaultTheme: Required<MarkdownTheme> = {
|
|
34
|
-
h1: Colors.brightCyan,
|
|
35
|
-
h2: Colors.cyan,
|
|
36
|
-
h3: Colors.brightBlue,
|
|
37
|
-
bold: Colors.brightWhite,
|
|
38
|
-
italic: Colors.brightYellow,
|
|
39
|
-
code: Colors.brightGreen,
|
|
40
|
-
codeBg: Colors.gray(2),
|
|
41
|
-
link: Colors.brightBlue,
|
|
42
|
-
linkUrl: Colors.gray(10),
|
|
43
|
-
quote: Colors.gray(14),
|
|
44
|
-
quoteBorder: Colors.gray(8),
|
|
45
|
-
listMarker: Colors.gray(12),
|
|
46
|
-
text: Colors.gray(18),
|
|
34
|
+
h1: Colors.ansi.brightCyan,
|
|
35
|
+
h2: Colors.cyan(500),
|
|
36
|
+
h3: Colors.ansi.brightBlue,
|
|
37
|
+
bold: Colors.ansi.brightWhite,
|
|
38
|
+
italic: Colors.ansi.brightYellow,
|
|
39
|
+
code: Colors.ansi.brightGreen,
|
|
40
|
+
codeBg: Colors.ansi.gray(2),
|
|
41
|
+
link: Colors.ansi.brightBlue,
|
|
42
|
+
linkUrl: Colors.ansi.gray(10),
|
|
43
|
+
quote: Colors.ansi.gray(14),
|
|
44
|
+
quoteBorder: Colors.ansi.gray(8),
|
|
45
|
+
listMarker: Colors.ansi.gray(12),
|
|
46
|
+
text: Colors.ansi.gray(18),
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface MarkdownProps {
|
|
@@ -262,12 +262,12 @@ export function MultilineTextInput({
|
|
|
262
262
|
placeholder = "",
|
|
263
263
|
width,
|
|
264
264
|
height = 5,
|
|
265
|
-
fg = Colors.brightWhite,
|
|
265
|
+
fg = Colors.ansi.brightWhite,
|
|
266
266
|
bg,
|
|
267
|
-
cursorFg = Colors.black,
|
|
268
|
-
cursorBg = Colors.brightWhite,
|
|
269
|
-
placeholderFg = Colors.gray(10),
|
|
270
|
-
lineNumberFg = Colors.gray(12),
|
|
267
|
+
cursorFg = Colors.ansi.black,
|
|
268
|
+
cursorBg = Colors.ansi.brightWhite,
|
|
269
|
+
placeholderFg = Colors.ansi.gray(10),
|
|
270
|
+
lineNumberFg = Colors.ansi.gray(12),
|
|
271
271
|
onCancel,
|
|
272
272
|
onSubmit,
|
|
273
273
|
focused = true,
|
package/src/components/Table.tsx
CHANGED
|
@@ -175,8 +175,8 @@ function TableRoot<T>({
|
|
|
175
175
|
headerSeparator = false,
|
|
176
176
|
rowSeparator = false,
|
|
177
177
|
padding = 1,
|
|
178
|
-
borderColor = Colors.gray(12),
|
|
179
|
-
headerColor = Colors.gray(12),
|
|
178
|
+
borderColor = Colors.ansi.gray(12),
|
|
179
|
+
headerColor = Colors.ansi.gray(12),
|
|
180
180
|
children,
|
|
181
181
|
}: TableProps<T>) {
|
|
182
182
|
const columns = extractColumns<T>(children, data)
|
|
@@ -73,11 +73,11 @@ export function TextInput({
|
|
|
73
73
|
onChange,
|
|
74
74
|
placeholder = "",
|
|
75
75
|
width,
|
|
76
|
-
fg = Colors.brightWhite,
|
|
76
|
+
fg = Colors.ansi.brightWhite,
|
|
77
77
|
bg,
|
|
78
|
-
cursorFg = Colors.black,
|
|
79
|
-
cursorBg = Colors.brightWhite,
|
|
80
|
-
placeholderFg = Colors.gray(10),
|
|
78
|
+
cursorFg = Colors.ansi.black,
|
|
79
|
+
cursorBg = Colors.ansi.brightWhite,
|
|
80
|
+
placeholderFg = Colors.ansi.gray(10),
|
|
81
81
|
onSubmit,
|
|
82
82
|
onCancel,
|
|
83
83
|
focused = true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Console capture singleton for TUI debugging
|
|
2
|
-
// Intercepts console.log/info/warn/error/debug
|
|
2
|
+
// Intercepts console.log/info/warn/error/debug AND process.stdout/stderr.write
|
|
3
|
+
// to capture ALL output including Effect.log, third-party libraries, etc.
|
|
3
4
|
|
|
4
5
|
import { Console } from "node:console"
|
|
5
6
|
import { EventEmitter } from "node:events"
|
|
@@ -105,17 +106,50 @@ export interface ConsoleCaptureEvents {
|
|
|
105
106
|
error: (entry: LogEntry) => void
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Store the ORIGINAL stdout/stderr write functions at module load time
|
|
110
|
+
// This ensures the renderer can always write to the terminal even when capture is active
|
|
111
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout)
|
|
112
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr)
|
|
113
|
+
|
|
108
114
|
export class ConsoleCapture extends EventEmitter {
|
|
109
115
|
private entries: LogEntry[] = []
|
|
110
116
|
private originalConsole: typeof console | null = null
|
|
111
117
|
private active = false
|
|
112
118
|
private maxEntries: number
|
|
119
|
+
// Track if current write is from our console methods (to avoid double-capture)
|
|
120
|
+
private inConsoleMethod = false
|
|
121
|
+
// Track if current write is from the TUI renderer (should bypass capture)
|
|
122
|
+
private inRendererWrite = false
|
|
123
|
+
// Block stdout/stderr writes only after renderer is ready
|
|
124
|
+
// Until then, pass through to avoid swallowing startup errors
|
|
125
|
+
private blockingEnabled = false
|
|
113
126
|
|
|
114
127
|
constructor(options?: { maxEntries?: number }) {
|
|
115
128
|
super()
|
|
116
129
|
this.maxEntries = options?.maxEntries ?? 1000
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Enable blocking of stdout/stderr writes.
|
|
134
|
+
* Call this after the renderer is fully set up and rendering.
|
|
135
|
+
*/
|
|
136
|
+
enableBlocking(): void {
|
|
137
|
+
this.blockingEnabled = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Write directly to stdout, bypassing capture.
|
|
142
|
+
* Used by the TUI renderer to output to terminal.
|
|
143
|
+
*/
|
|
144
|
+
writeToTerminal(data: string | Uint8Array): boolean {
|
|
145
|
+
this.inRendererWrite = true
|
|
146
|
+
try {
|
|
147
|
+
return originalStdoutWrite(data)
|
|
148
|
+
} finally {
|
|
149
|
+
this.inRendererWrite = false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
get isActive(): boolean {
|
|
120
154
|
return this.active
|
|
121
155
|
}
|
|
@@ -134,6 +168,57 @@ export class ConsoleCapture extends EventEmitter {
|
|
|
134
168
|
// Store original console
|
|
135
169
|
this.originalConsole = globalThis.console
|
|
136
170
|
|
|
171
|
+
// Enable blocking immediately - TUI owns the terminal
|
|
172
|
+
// Any output will be captured in the console panel, not displayed
|
|
173
|
+
this.blockingEnabled = true
|
|
174
|
+
|
|
175
|
+
// Intercept stdout.write - captures Effect.log, printf, etc.
|
|
176
|
+
process.stdout.write = ((
|
|
177
|
+
chunk: Uint8Array | string,
|
|
178
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
179
|
+
callback?: (err?: Error | null) => void,
|
|
180
|
+
): boolean => {
|
|
181
|
+
// If this write is from renderer, pass through
|
|
182
|
+
if (this.inRendererWrite) {
|
|
183
|
+
return originalStdoutWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
184
|
+
}
|
|
185
|
+
// Capture if not from our console methods (avoid double-capture)
|
|
186
|
+
if (!this.inConsoleMethod) {
|
|
187
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString()
|
|
188
|
+
this.captureRawOutput(text, "stdout")
|
|
189
|
+
}
|
|
190
|
+
// If blocking not enabled yet, pass through (allows startup errors to show)
|
|
191
|
+
if (!this.blockingEnabled) {
|
|
192
|
+
return originalStdoutWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
193
|
+
}
|
|
194
|
+
// Block write - TUI owns the terminal
|
|
195
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback
|
|
196
|
+
if (cb) cb()
|
|
197
|
+
return true
|
|
198
|
+
}) as typeof process.stdout.write
|
|
199
|
+
|
|
200
|
+
// Intercept stderr.write
|
|
201
|
+
process.stderr.write = ((
|
|
202
|
+
chunk: Uint8Array | string,
|
|
203
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
204
|
+
callback?: (err?: Error | null) => void,
|
|
205
|
+
): boolean => {
|
|
206
|
+
if (this.inRendererWrite) {
|
|
207
|
+
return originalStderrWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
208
|
+
}
|
|
209
|
+
if (!this.inConsoleMethod) {
|
|
210
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString()
|
|
211
|
+
this.captureRawOutput(text, "stderr")
|
|
212
|
+
}
|
|
213
|
+
// If blocking not enabled yet, pass through (allows startup errors to show)
|
|
214
|
+
if (!this.blockingEnabled) {
|
|
215
|
+
return originalStderrWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
216
|
+
}
|
|
217
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback
|
|
218
|
+
if (cb) cb()
|
|
219
|
+
return true
|
|
220
|
+
}) as typeof process.stdout.write
|
|
221
|
+
|
|
137
222
|
// Create mock streams for the new console
|
|
138
223
|
const mockStdout = new CapturedWritableStream()
|
|
139
224
|
const mockStderr = new CapturedWritableStream()
|
|
@@ -197,37 +282,95 @@ export class ConsoleCapture extends EventEmitter {
|
|
|
197
282
|
deactivate(): void {
|
|
198
283
|
if (!this.active || !this.originalConsole) return
|
|
199
284
|
|
|
285
|
+
// Restore original stdout/stderr write methods
|
|
286
|
+
process.stdout.write = originalStdoutWrite
|
|
287
|
+
process.stderr.write = originalStderrWrite
|
|
288
|
+
|
|
200
289
|
globalThis.console = this.originalConsole
|
|
201
290
|
this.originalConsole = null
|
|
202
291
|
this.active = false
|
|
203
292
|
}
|
|
204
293
|
|
|
205
294
|
private appendToConsole(level: LogLevel, args: unknown[]): void {
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
// Mark that we're in a console method to avoid double-capture from stdout/stderr
|
|
296
|
+
this.inConsoleMethod = true
|
|
297
|
+
try {
|
|
298
|
+
const location = captureCallerLocation()
|
|
299
|
+
const message = util.format(...args)
|
|
300
|
+
|
|
301
|
+
const entry: LogEntry = {
|
|
302
|
+
timestamp: new Date(),
|
|
303
|
+
level,
|
|
304
|
+
message,
|
|
305
|
+
args,
|
|
306
|
+
location,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Enforce max entries
|
|
310
|
+
if (this.entries.length >= this.maxEntries) {
|
|
311
|
+
this.entries.shift()
|
|
312
|
+
}
|
|
313
|
+
this.entries.push(entry)
|
|
314
|
+
|
|
315
|
+
// Emit events
|
|
316
|
+
this.emit("entry", entry)
|
|
317
|
+
// Only emit error event if there are listeners (avoid unhandled error exception)
|
|
318
|
+
if (level === "ERROR" && this.listenerCount("error") > 0) {
|
|
319
|
+
this.emit("error", entry)
|
|
320
|
+
}
|
|
321
|
+
} finally {
|
|
322
|
+
this.inConsoleMethod = false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Capture raw output from stdout/stderr (for Effect.log, third-party libs, etc.)
|
|
327
|
+
private captureRawOutput(text: string, stream: "stdout" | "stderr"): void {
|
|
328
|
+
// Skip empty output
|
|
329
|
+
if (!text.trim()) return
|
|
330
|
+
|
|
331
|
+
// Parse Effect log format: timestamp level fiber message
|
|
332
|
+
// Example: "timestamp=2024-01-15T10:30:00.000Z level=INFO fiber=#0 message=Hello"
|
|
333
|
+
// Or the pretty format with ANSI colors
|
|
334
|
+
const level = this.detectLogLevel(text, stream)
|
|
335
|
+
|
|
336
|
+
// Strip ANSI codes for the stored message
|
|
337
|
+
const cleanText = text.replace(/\x1b\[[0-9;]*m/g, "").trim()
|
|
208
338
|
|
|
209
339
|
const entry: LogEntry = {
|
|
210
340
|
timestamp: new Date(),
|
|
211
341
|
level,
|
|
212
|
-
message,
|
|
213
|
-
args,
|
|
214
|
-
location
|
|
342
|
+
message: cleanText,
|
|
343
|
+
args: [text], // Keep original with ANSI for potential future use
|
|
344
|
+
// No location for raw output - we can't get stack trace here
|
|
215
345
|
}
|
|
216
346
|
|
|
217
|
-
// Enforce max entries
|
|
218
347
|
if (this.entries.length >= this.maxEntries) {
|
|
219
348
|
this.entries.shift()
|
|
220
349
|
}
|
|
221
350
|
this.entries.push(entry)
|
|
222
351
|
|
|
223
|
-
// Emit events
|
|
224
352
|
this.emit("entry", entry)
|
|
225
|
-
// Only emit error event if there are listeners (avoid unhandled error exception)
|
|
226
353
|
if (level === "ERROR" && this.listenerCount("error") > 0) {
|
|
227
354
|
this.emit("error", entry)
|
|
228
355
|
}
|
|
229
356
|
}
|
|
230
357
|
|
|
358
|
+
// Detect log level from raw output text
|
|
359
|
+
private detectLogLevel(text: string, stream: "stdout" | "stderr"): LogLevel {
|
|
360
|
+
const upperText = text.toUpperCase()
|
|
361
|
+
|
|
362
|
+
// Effect log format detection
|
|
363
|
+
if (upperText.includes("LEVEL=ERROR") || upperText.includes("[ERROR]")) return "ERROR"
|
|
364
|
+
if (upperText.includes("LEVEL=WARN") || upperText.includes("[WARN]")) return "WARN"
|
|
365
|
+
if (upperText.includes("LEVEL=INFO") || upperText.includes("[INFO]")) return "INFO"
|
|
366
|
+
if (upperText.includes("LEVEL=DEBUG") || upperText.includes("[DEBUG]")) return "DEBUG"
|
|
367
|
+
|
|
368
|
+
// stderr is typically error output
|
|
369
|
+
if (stream === "stderr") return "ERROR"
|
|
370
|
+
|
|
371
|
+
return "LOG"
|
|
372
|
+
}
|
|
373
|
+
|
|
231
374
|
// Also expose write to original console for debugging the capture itself
|
|
232
375
|
writeToOriginal(level: LogLevel, ...args: unknown[]): void {
|
|
233
376
|
if (!this.originalConsole) return
|
|
@@ -264,6 +407,28 @@ export function getConsoleCapture(): ConsoleCapture {
|
|
|
264
407
|
return instance
|
|
265
408
|
}
|
|
266
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Write directly to the terminal, bypassing console capture.
|
|
412
|
+
* Used by the TUI renderer to output to terminal without being captured.
|
|
413
|
+
*/
|
|
414
|
+
export function writeToTerminal(data: string | Uint8Array): boolean {
|
|
415
|
+
return originalStdoutWrite(data)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a wrapped stdout that bypasses console capture.
|
|
420
|
+
* Returns a write function compatible with TuiWriteStream.
|
|
421
|
+
*/
|
|
422
|
+
export function createTerminalWriter(): typeof process.stdout.write {
|
|
423
|
+
return ((
|
|
424
|
+
chunk: Uint8Array | string,
|
|
425
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
426
|
+
callback?: (err?: Error | null) => void,
|
|
427
|
+
): boolean => {
|
|
428
|
+
return originalStdoutWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
429
|
+
}) as typeof process.stdout.write
|
|
430
|
+
}
|
|
431
|
+
|
|
267
432
|
// Clean up on process exit
|
|
268
433
|
process.on("exit", () => {
|
|
269
434
|
if (instance?.isActive) {
|
|
@@ -41,16 +41,16 @@ interface SelectionPoint {
|
|
|
41
41
|
// ─────────────────────────────────────────────────────────────
|
|
42
42
|
|
|
43
43
|
const LOG_COLORS: Record<LogLevel, ColorValue> = {
|
|
44
|
-
LOG: Colors.white,
|
|
45
|
-
INFO: Colors.cyan,
|
|
46
|
-
WARN: Colors.yellow,
|
|
47
|
-
ERROR: Colors.red,
|
|
48
|
-
DEBUG: Colors.gray(12),
|
|
44
|
+
LOG: Colors.ansi.white,
|
|
45
|
+
INFO: Colors.cyan(500),
|
|
46
|
+
WARN: Colors.yellow(500),
|
|
47
|
+
ERROR: Colors.red(500),
|
|
48
|
+
DEBUG: Colors.ansi.gray(12),
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const TITLE_BG = Colors.gray(4)
|
|
52
|
-
const TITLE_FG = Colors.white
|
|
53
|
-
const CONTENT_BG = Colors.gray(2)
|
|
51
|
+
const TITLE_BG = Colors.ansi.gray(4)
|
|
52
|
+
const TITLE_FG = Colors.ansi.white
|
|
53
|
+
const CONTENT_BG = Colors.ansi.gray(2)
|
|
54
54
|
const SELECTION_BG = Colors.rgb(60, 90, 140)
|
|
55
55
|
|
|
56
56
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -421,7 +421,7 @@ export function ConsolePopover({
|
|
|
421
421
|
{before}
|
|
422
422
|
</text>
|
|
423
423
|
)}
|
|
424
|
-
<text fg={Colors.white} bg={SELECTION_BG}>
|
|
424
|
+
<text fg={Colors.ansi.white} bg={SELECTION_BG}>
|
|
425
425
|
{selected}
|
|
426
426
|
</text>
|
|
427
427
|
{after.length > 0 && (
|
|
@@ -450,11 +450,11 @@ export function ConsolePopover({
|
|
|
450
450
|
<vstack width={termWidth} height={popoverHeight}>
|
|
451
451
|
{/* Title bar */}
|
|
452
452
|
<hstack height={1} bg={TITLE_BG}>
|
|
453
|
-
<text fg={feedback ? Colors.green : TITLE_FG} bg={TITLE_BG}>
|
|
453
|
+
<text fg={feedback ? Colors.green(500) : TITLE_FG} bg={TITLE_BG}>
|
|
454
454
|
{` ${titleText}`}
|
|
455
455
|
</text>
|
|
456
456
|
<spacer />
|
|
457
|
-
<text fg={Colors.gray(14)} bg={TITLE_BG}>
|
|
457
|
+
<text fg={Colors.ansi.gray(14)} bg={TITLE_BG}>
|
|
458
458
|
{`${mode === "inline" ? inlineHints : hints} `}
|
|
459
459
|
</text>
|
|
460
460
|
</hstack>
|
|
@@ -463,7 +463,7 @@ export function ConsolePopover({
|
|
|
463
463
|
<scroll {...scrollProps} sticky>
|
|
464
464
|
<vstack>
|
|
465
465
|
{displayLines.length === 0 ? (
|
|
466
|
-
<text fg={Colors.gray(10)} bg={CONTENT_BG}>
|
|
466
|
+
<text fg={Colors.ansi.gray(10)} bg={CONTENT_BG}>
|
|
467
467
|
No console output yet
|
|
468
468
|
</text>
|
|
469
469
|
) : (
|
|
@@ -65,16 +65,13 @@ export function useConsole(options: UseConsoleOptions = {}): UseConsoleReturn {
|
|
|
65
65
|
const [visible, setVisible] = useState(initiallyVisible)
|
|
66
66
|
const [entries, setEntries] = useState<LogEntry[]>(() => capture.getEntries())
|
|
67
67
|
|
|
68
|
-
// Activate capture on mount
|
|
68
|
+
// Activate capture on mount (blocking is enabled immediately in activate())
|
|
69
69
|
useEffect(() => {
|
|
70
70
|
if (!capture.isActive) {
|
|
71
71
|
capture.activate()
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Don't deactivate on unmount - other components may still use it
|
|
76
|
-
// The singleton handles cleanup on process exit
|
|
77
|
-
}
|
|
73
|
+
// Don't deactivate on unmount - other components may still use it
|
|
74
|
+
// The singleton handles cleanup on process exit
|
|
78
75
|
}, [capture])
|
|
79
76
|
|
|
80
77
|
// Subscribe to entries and errors
|
|
@@ -17,21 +17,21 @@ export function DiagnosticsPanel({ sampleMs = 200, title = "Diagnostics" }: Diag
|
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<vstack spacing={1}>
|
|
20
|
-
<text fg={Colors.cyan} bold>
|
|
20
|
+
<text fg={Colors.cyan(500)} bold>
|
|
21
21
|
{title} ({stats?.mode ?? "?"})
|
|
22
22
|
</text>
|
|
23
23
|
{stats ? (
|
|
24
24
|
<>
|
|
25
|
-
<text fg={Colors.gray(12)}>
|
|
25
|
+
<text fg={Colors.ansi.gray(12)}>
|
|
26
26
|
size {stats.width}×{stats.height} content {h} rows — {bytes} bytes/frame — {fps} fps
|
|
27
27
|
</text>
|
|
28
|
-
<text fg={Colors.gray(12)}>
|
|
28
|
+
<text fg={Colors.ansi.gray(12)}>
|
|
29
29
|
clear {phases?.clear.toFixed(2)}ms · layout {phases?.layout.toFixed(2)}ms · render{" "}
|
|
30
30
|
{phases?.render.toFixed(2)}ms · diff {phases?.diffAnsi.toFixed(2)}ms · write {phases?.write.toFixed(2)}ms
|
|
31
31
|
</text>
|
|
32
32
|
</>
|
|
33
33
|
) : (
|
|
34
|
-
<text fg={Colors.gray(10)}>Waiting for first frame…</text>
|
|
34
|
+
<text fg={Colors.ansi.gray(10)}>Waiting for first frame…</text>
|
|
35
35
|
)}
|
|
36
36
|
</vstack>
|
|
37
37
|
)
|
package/src/dev/Toast.tsx
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import { Colors } from "@effect-tui/core"
|
|
5
5
|
import { createContext, type ReactNode, useCallback, useContext, useState } from "react"
|
|
6
|
-
import { useTerminalSize } from "../renderer-context.js"
|
|
7
6
|
|
|
8
7
|
// ─────────────────────────────────────────────────────────────
|
|
9
8
|
// Types
|
|
@@ -82,7 +81,6 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
82
81
|
|
|
83
82
|
export function ToastContainer() {
|
|
84
83
|
const { toasts } = useToast()
|
|
85
|
-
const { width } = useTerminalSize()
|
|
86
84
|
|
|
87
85
|
if (toasts.length === 0) return null
|
|
88
86
|
|
|
@@ -90,16 +88,17 @@ export function ToastContainer() {
|
|
|
90
88
|
const toast = toasts[toasts.length - 1]
|
|
91
89
|
const style = TOAST_STYLES[toast.type]
|
|
92
90
|
|
|
93
|
-
//
|
|
91
|
+
// Compact pill in upper right
|
|
94
92
|
const content = ` ${style.icon} ${toast.message} `
|
|
95
|
-
const padding = Math.max(0, Math.floor((width - content.length) / 2))
|
|
96
|
-
const paddedContent = " ".repeat(padding) + content + " ".repeat(width - padding - content.length)
|
|
97
93
|
|
|
98
|
-
//
|
|
94
|
+
// Position in upper right corner
|
|
99
95
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
<hstack>
|
|
97
|
+
<spacer />
|
|
98
|
+
<box bg={style.bg} padding={{ x: 1 }}>
|
|
99
|
+
<text fg={style.fg}>{content}</text>
|
|
100
|
+
</box>
|
|
101
|
+
</hstack>
|
|
103
102
|
)
|
|
104
103
|
}
|
|
105
104
|
|
package/src/dev.tsx
CHANGED
|
@@ -327,7 +327,7 @@ function StatsOverlay({ sampleMs, title }: { sampleMs?: number; title?: string }
|
|
|
327
327
|
<vstack width={width} height={height}>
|
|
328
328
|
<hstack width={width}>
|
|
329
329
|
<spacer />
|
|
330
|
-
<box padding={1} border="rounded" borderColor={Colors.gray(8)} bg={Colors.gray(2)}>
|
|
330
|
+
<box padding={1} border="rounded" borderColor={Colors.ansi.gray(8)} bg={Colors.ansi.gray(2)}>
|
|
331
331
|
<DiagnosticsPanel sampleMs={sampleMs} title={title ?? "Renderer Stats"} />
|
|
332
332
|
</box>
|
|
333
333
|
</hstack>
|
package/src/hooks/use-scroll.ts
CHANGED
|
@@ -218,28 +218,37 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
|
|
|
218
218
|
const atStart = offset <= 0
|
|
219
219
|
const atEnd = offset >= maxOffset
|
|
220
220
|
|
|
221
|
+
const clampOffset = useCallback(
|
|
222
|
+
(value: number) => {
|
|
223
|
+
const clamped = Math.max(0, Math.min(maxOffset, value))
|
|
224
|
+
// Keep refs in sync so rapid events don't use stale state
|
|
225
|
+
offsetRef.current = clamped
|
|
226
|
+
wasAtEndRef.current = clamped >= maxOffset - 1
|
|
227
|
+
return clamped
|
|
228
|
+
},
|
|
229
|
+
[maxOffset],
|
|
230
|
+
)
|
|
231
|
+
|
|
221
232
|
// Clamp and set offset
|
|
222
233
|
const setOffset = useCallback(
|
|
223
234
|
(newOffset: number) => {
|
|
224
|
-
|
|
225
|
-
setOffsetRaw(clamped)
|
|
226
|
-
// Track if we're at end for sticky behavior
|
|
227
|
-
wasAtEndRef.current = clamped >= maxOffset - 1
|
|
235
|
+
setOffsetRaw(() => clampOffset(newOffset))
|
|
228
236
|
},
|
|
229
|
-
[
|
|
237
|
+
[clampOffset],
|
|
230
238
|
)
|
|
231
239
|
|
|
232
240
|
// Scroll by delta with accumulator for fractional scrolling
|
|
241
|
+
// Uses functional state updates to avoid stale closures during rapid scrolling
|
|
233
242
|
const scrollBy = useCallback(
|
|
234
243
|
(delta: number) => {
|
|
235
244
|
accumulatorRef.current += delta
|
|
236
245
|
const integerDelta = Math.trunc(accumulatorRef.current)
|
|
237
246
|
if (integerDelta !== 0) {
|
|
238
|
-
|
|
247
|
+
setOffsetRaw((prev) => clampOffset(prev + integerDelta))
|
|
239
248
|
accumulatorRef.current -= integerDelta
|
|
240
249
|
}
|
|
241
250
|
},
|
|
242
|
-
[
|
|
251
|
+
[clampOffset],
|
|
243
252
|
)
|
|
244
253
|
|
|
245
254
|
const scrollToStart = useCallback(() => {
|
|
@@ -270,9 +279,9 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
|
|
|
270
279
|
if (!sticky) return
|
|
271
280
|
const newMaxOffset = Math.max(0, contentSize - viewportSize)
|
|
272
281
|
if (wasAtEndRef.current && contentSize > 0) {
|
|
273
|
-
setOffsetRaw(newMaxOffset)
|
|
282
|
+
setOffsetRaw(() => clampOffset(newMaxOffset))
|
|
274
283
|
}
|
|
275
|
-
}, [sticky, contentSize, viewportSize])
|
|
284
|
+
}, [sticky, contentSize, viewportSize, clampOffset])
|
|
276
285
|
|
|
277
286
|
// Handle viewport size changes (reported by scroll component)
|
|
278
287
|
const handleViewportSize = useCallback(
|
package/src/hosts/box.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface BoxProps extends CommonProps {
|
|
|
20
20
|
border?: BorderKind
|
|
21
21
|
borderColor?: Color
|
|
22
22
|
bg?: Color
|
|
23
|
+
/** Title displayed on the top border */
|
|
24
|
+
title?: string
|
|
25
|
+
/** Title text color (defaults to borderColor) */
|
|
26
|
+
titleColor?: Color
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export class BoxHost extends SingleChildHost {
|
|
@@ -27,6 +31,8 @@ export class BoxHost extends SingleChildHost {
|
|
|
27
31
|
border: BorderKind = "none"
|
|
28
32
|
borderColor?: Color
|
|
29
33
|
bg?: Color
|
|
34
|
+
title?: string
|
|
35
|
+
titleColor?: Color
|
|
30
36
|
|
|
31
37
|
constructor(props: BoxProps, ctx: HostContext) {
|
|
32
38
|
super("box", props, ctx)
|
|
@@ -101,9 +107,21 @@ export class BoxHost extends SingleChildHost {
|
|
|
101
107
|
// Draw border
|
|
102
108
|
if (this.border !== "none" && w >= 2 && h >= 2) {
|
|
103
109
|
const chars = borderChars(this.border)
|
|
104
|
-
const borderFg = toColorValue(this.borderColor) ?? Colors.gray(8)
|
|
110
|
+
const borderFg = toColorValue(this.borderColor) ?? Colors.ansi.gray(8)
|
|
105
111
|
const borderStyle = palette.id({ fg: borderFg })
|
|
106
112
|
drawBorder(buffer, x, y, w, h, chars, borderStyle)
|
|
113
|
+
|
|
114
|
+
// Draw title on top border if present
|
|
115
|
+
if (this.title && w >= 5) {
|
|
116
|
+
const titleFg = toColorValue(this.titleColor) ?? borderFg
|
|
117
|
+
const titleStyle = palette.id({ fg: titleFg })
|
|
118
|
+
const maxTitleLen = w - 4 // Leave room for "─ " and " ─"
|
|
119
|
+
const displayTitle =
|
|
120
|
+
this.title.length > maxTitleLen ? this.title.slice(0, maxTitleLen - 1) + "…" : this.title
|
|
121
|
+
// Draw " Title " starting at x+2
|
|
122
|
+
const titleX = x + 2
|
|
123
|
+
buffer.drawText(titleX, y, ` ${displayTitle} `, titleStyle)
|
|
124
|
+
}
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
// Render single child
|
|
@@ -119,5 +137,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
119
137
|
this.border = (props.border as BorderKind | undefined) ?? "none"
|
|
120
138
|
this.borderColor = props.borderColor as Color | undefined
|
|
121
139
|
this.bg = props.bg as Color | undefined
|
|
140
|
+
this.title = props.title as string | undefined
|
|
141
|
+
this.titleColor = props.titleColor as Color | undefined
|
|
122
142
|
}
|
|
123
143
|
}
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class CanvasHost extends BaseHost {
|
|
|
168
168
|
// Draw border (with clipping)
|
|
169
169
|
if (border !== "none" && bw >= 2 && bh >= 2) {
|
|
170
170
|
const chars = borderChars(border)
|
|
171
|
-
const borderFg = toColorValue(opts?.borderColor) ?? toColorValue(opts?.fg) ?? Colors.gray(8)
|
|
171
|
+
const borderFg = toColorValue(opts?.borderColor) ?? toColorValue(opts?.fg) ?? Colors.ansi.gray(8)
|
|
172
172
|
const borderStyle = palette.id({ fg: borderFg })
|
|
173
173
|
drawBorder(buffer, px, py, bw, bh, chars, borderStyle, { ox, oy, w, h })
|
|
174
174
|
}
|
package/src/hosts/codeblock.ts
CHANGED
|
@@ -88,7 +88,7 @@ export class CodeBlockHost extends BaseHost {
|
|
|
88
88
|
|
|
89
89
|
if (this.lineNumbers) {
|
|
90
90
|
const gutterStyle = styleIdFromProps(palette, {
|
|
91
|
-
fg: this.lineNumberColor ?? Colors.gray(11),
|
|
91
|
+
fg: this.lineNumberColor ?? Colors.ansi.gray(11),
|
|
92
92
|
bg: this.lineNumberBackground ?? this.background,
|
|
93
93
|
})
|
|
94
94
|
const digits = String(i + 1).padStart(this.gutterWidth - 1, " ")
|
package/src/inline/index.tsx
CHANGED
|
@@ -41,19 +41,19 @@ function TaskList({ tasks, spinnerIndex }: TaskListProps) {
|
|
|
41
41
|
|
|
42
42
|
const color =
|
|
43
43
|
task.status === "running"
|
|
44
|
-
? Colors.brightYellow
|
|
44
|
+
? Colors.ansi.brightYellow
|
|
45
45
|
: task.status === "success"
|
|
46
|
-
? Colors.brightGreen
|
|
46
|
+
? Colors.ansi.brightGreen
|
|
47
47
|
: task.status === "failure"
|
|
48
|
-
? Colors.brightRed
|
|
49
|
-
: Colors.gray(10)
|
|
48
|
+
? Colors.ansi.brightRed
|
|
49
|
+
: Colors.ansi.gray(10)
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<hstack key={i} spacing={1}>
|
|
53
53
|
<text bold fg={color}>
|
|
54
54
|
{icon}
|
|
55
55
|
</text>
|
|
56
|
-
<text fg={task.status === "pending" ? Colors.gray(10) : undefined}>{task.label}</text>
|
|
56
|
+
<text fg={task.status === "pending" ? Colors.ansi.gray(10) : undefined}>{task.label}</text>
|
|
57
57
|
</hstack>
|
|
58
58
|
)
|
|
59
59
|
})}
|