@vybestack/llxprt-ui 0.7.0-nightly.251211.5750c518a

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.
Files changed (123) hide show
  1. package/PLAN-messages.md +681 -0
  2. package/PLAN.md +47 -0
  3. package/README.md +25 -0
  4. package/bun.lock +1024 -0
  5. package/dev-docs/ARCHITECTURE.md +178 -0
  6. package/dev-docs/CODE_ORGANIZATION.md +232 -0
  7. package/dev-docs/STANDARDS.md +235 -0
  8. package/dev-docs/UI_DESIGN.md +425 -0
  9. package/eslint.config.cjs +194 -0
  10. package/images/nui.png +0 -0
  11. package/llxprt.png +0 -0
  12. package/llxprt.svg +128 -0
  13. package/package.json +66 -0
  14. package/scripts/check-limits.ts +177 -0
  15. package/scripts/start.js +71 -0
  16. package/src/app.tsx +599 -0
  17. package/src/bootstrap.tsx +23 -0
  18. package/src/commands/AuthCommand.tsx +80 -0
  19. package/src/commands/ModelCommand.tsx +102 -0
  20. package/src/commands/ProviderCommand.tsx +103 -0
  21. package/src/commands/ThemeCommand.tsx +71 -0
  22. package/src/features/chat/history.ts +178 -0
  23. package/src/features/chat/index.ts +3 -0
  24. package/src/features/chat/persistentHistory.ts +102 -0
  25. package/src/features/chat/responder.ts +217 -0
  26. package/src/features/completion/completions.ts +161 -0
  27. package/src/features/completion/index.ts +3 -0
  28. package/src/features/completion/slash.test.ts +82 -0
  29. package/src/features/completion/slash.ts +248 -0
  30. package/src/features/completion/suggestions.test.ts +51 -0
  31. package/src/features/completion/suggestions.ts +112 -0
  32. package/src/features/config/configSession.test.ts +189 -0
  33. package/src/features/config/configSession.ts +179 -0
  34. package/src/features/config/index.ts +4 -0
  35. package/src/features/config/llxprtAdapter.integration.test.ts +202 -0
  36. package/src/features/config/llxprtAdapter.test.ts +139 -0
  37. package/src/features/config/llxprtAdapter.ts +257 -0
  38. package/src/features/config/llxprtCommands.test.ts +40 -0
  39. package/src/features/config/llxprtCommands.ts +35 -0
  40. package/src/features/config/llxprtConfig.test.ts +261 -0
  41. package/src/features/config/llxprtConfig.ts +418 -0
  42. package/src/features/theme/index.ts +2 -0
  43. package/src/features/theme/theme.test.ts +51 -0
  44. package/src/features/theme/theme.ts +105 -0
  45. package/src/features/theme/themeManager.ts +84 -0
  46. package/src/hooks/useAppCommands.ts +129 -0
  47. package/src/hooks/useApprovalKeyboard.ts +156 -0
  48. package/src/hooks/useChatStore.test.ts +112 -0
  49. package/src/hooks/useChatStore.ts +252 -0
  50. package/src/hooks/useInputManager.ts +99 -0
  51. package/src/hooks/useKeyboardHandlers.ts +130 -0
  52. package/src/hooks/useListNavigation.test.ts +166 -0
  53. package/src/hooks/useListNavigation.ts +62 -0
  54. package/src/hooks/usePersistentHistory.ts +94 -0
  55. package/src/hooks/useScrollManagement.ts +107 -0
  56. package/src/hooks/useSelectionClipboard.ts +48 -0
  57. package/src/hooks/useSessionManager.test.ts +85 -0
  58. package/src/hooks/useSessionManager.ts +101 -0
  59. package/src/hooks/useStreamingLifecycle.ts +71 -0
  60. package/src/hooks/useStreamingResponder.ts +401 -0
  61. package/src/hooks/useSuggestionSetup.ts +23 -0
  62. package/src/hooks/useToolApproval.test.ts +140 -0
  63. package/src/hooks/useToolApproval.ts +264 -0
  64. package/src/hooks/useToolScheduler.ts +432 -0
  65. package/src/index.ts +3 -0
  66. package/src/jsx.d.ts +11 -0
  67. package/src/lib/clipboard.ts +18 -0
  68. package/src/lib/logger.ts +107 -0
  69. package/src/lib/random.ts +5 -0
  70. package/src/main.tsx +13 -0
  71. package/src/test/mockTheme.ts +51 -0
  72. package/src/types/events.ts +87 -0
  73. package/src/types.ts +13 -0
  74. package/src/ui/components/ChatLayout.tsx +694 -0
  75. package/src/ui/components/CommandComponents.tsx +74 -0
  76. package/src/ui/components/DiffViewer.tsx +306 -0
  77. package/src/ui/components/FilterInput.test.ts +69 -0
  78. package/src/ui/components/FilterInput.tsx +62 -0
  79. package/src/ui/components/HeaderBar.tsx +137 -0
  80. package/src/ui/components/RadioSelect.test.ts +140 -0
  81. package/src/ui/components/RadioSelect.tsx +88 -0
  82. package/src/ui/components/SelectableList.test.ts +83 -0
  83. package/src/ui/components/SelectableList.tsx +35 -0
  84. package/src/ui/components/StatusBar.tsx +45 -0
  85. package/src/ui/components/SuggestionPanel.tsx +102 -0
  86. package/src/ui/components/messages/ModelMessage.tsx +14 -0
  87. package/src/ui/components/messages/SystemMessage.tsx +29 -0
  88. package/src/ui/components/messages/ThinkingMessage.tsx +14 -0
  89. package/src/ui/components/messages/UserMessage.tsx +26 -0
  90. package/src/ui/components/messages/index.ts +15 -0
  91. package/src/ui/components/messages/renderMessage.test.ts +49 -0
  92. package/src/ui/components/messages/renderMessage.tsx +43 -0
  93. package/src/ui/components/messages/types.test.ts +24 -0
  94. package/src/ui/components/messages/types.ts +36 -0
  95. package/src/ui/modals/AuthModal.tsx +106 -0
  96. package/src/ui/modals/ModalShell.tsx +60 -0
  97. package/src/ui/modals/SearchSelectModal.tsx +236 -0
  98. package/src/ui/modals/ThemeModal.tsx +204 -0
  99. package/src/ui/modals/ToolApprovalModal.test.ts +206 -0
  100. package/src/ui/modals/ToolApprovalModal.tsx +282 -0
  101. package/src/ui/modals/index.ts +20 -0
  102. package/src/ui/modals/modals.test.ts +26 -0
  103. package/src/ui/modals/types.ts +19 -0
  104. package/src/uicontext/Command.tsx +102 -0
  105. package/src/uicontext/Dialog.tsx +65 -0
  106. package/src/uicontext/index.ts +2 -0
  107. package/themes/ansi-light.json +59 -0
  108. package/themes/ansi.json +59 -0
  109. package/themes/atom-one-dark.json +59 -0
  110. package/themes/ayu-light.json +59 -0
  111. package/themes/ayu.json +59 -0
  112. package/themes/default-light.json +59 -0
  113. package/themes/default.json +59 -0
  114. package/themes/dracula.json +59 -0
  115. package/themes/github-dark.json +59 -0
  116. package/themes/github-light.json +59 -0
  117. package/themes/googlecode.json +59 -0
  118. package/themes/green-screen.json +59 -0
  119. package/themes/no-color.json +59 -0
  120. package/themes/shades-of-purple.json +59 -0
  121. package/themes/xcode.json +59 -0
  122. package/tsconfig.json +28 -0
  123. package/vitest.config.ts +10 -0
@@ -0,0 +1,432 @@
1
+ import { useCallback, useState, useRef, useEffect } from 'react';
2
+ import type { Config, ToolCallRequestInfo } from '@vybestack/llxprt-code-core';
3
+ import {
4
+ CoreToolScheduler,
5
+ type ToolCall as CoreToolCall,
6
+ type CompletedToolCall,
7
+ type WaitingToolCall,
8
+ type ExecutingToolCall,
9
+ type CancelledToolCall,
10
+ type ToolCallConfirmationDetails,
11
+ type ToolConfirmationOutcome,
12
+ } from '@vybestack/llxprt-code-core';
13
+ import type { ToolStatus } from '../types/events';
14
+ import { getLogger } from '../lib/logger';
15
+
16
+ const logger = getLogger('nui:tool-scheduler');
17
+
18
+ /**
19
+ * Tracked tool call with response submission state
20
+ */
21
+ export type TrackedToolCall = CoreToolCall & {
22
+ responseSubmittedToModel?: boolean;
23
+ };
24
+
25
+ export type TrackedCompletedToolCall = CompletedToolCall & {
26
+ responseSubmittedToModel?: boolean;
27
+ };
28
+
29
+ export type TrackedCancelledToolCall = CancelledToolCall & {
30
+ responseSubmittedToModel?: boolean;
31
+ };
32
+
33
+ export type TrackedWaitingToolCall = WaitingToolCall & {
34
+ responseSubmittedToModel?: boolean;
35
+ };
36
+
37
+ export type TrackedExecutingToolCall = ExecutingToolCall & {
38
+ responseSubmittedToModel?: boolean;
39
+ liveOutput?: string;
40
+ };
41
+
42
+ /**
43
+ * Tool call display info for the UI
44
+ */
45
+ export interface ToolCallDisplayInfo {
46
+ callId: string;
47
+ name: string;
48
+ displayName: string;
49
+ description: string;
50
+ status: ToolStatus;
51
+ output?: string;
52
+ errorMessage?: string;
53
+ confirmationDetails?: ToolCallConfirmationDetails;
54
+ }
55
+
56
+ export type ScheduleFn = (
57
+ request: ToolCallRequestInfo | ToolCallRequestInfo[],
58
+ signal: AbortSignal,
59
+ ) => void;
60
+
61
+ export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
62
+ export type CancelAllFn = () => void;
63
+ export type RespondToConfirmationFn = (
64
+ callId: string,
65
+ outcome: ToolConfirmationOutcome,
66
+ ) => void;
67
+
68
+ export interface UseToolSchedulerResult {
69
+ toolCalls: TrackedToolCall[];
70
+ schedule: ScheduleFn;
71
+ markToolsAsSubmitted: MarkToolsAsSubmittedFn;
72
+ cancelAll: CancelAllFn;
73
+ getToolDisplayInfo: () => ToolCallDisplayInfo[];
74
+ respondToConfirmation: RespondToConfirmationFn;
75
+ }
76
+
77
+ /**
78
+ * Map CoreToolScheduler status to UI ToolStatus
79
+ */
80
+ function mapCoreStatusToToolStatus(status: CoreToolCall['status']): ToolStatus {
81
+ switch (status) {
82
+ case 'scheduled':
83
+ return 'pending';
84
+ case 'validating':
85
+ return 'executing';
86
+ case 'awaiting_approval':
87
+ return 'confirming';
88
+ case 'executing':
89
+ return 'executing';
90
+ case 'success':
91
+ return 'complete';
92
+ case 'error':
93
+ return 'error';
94
+ case 'cancelled':
95
+ return 'cancelled';
96
+ default:
97
+ return 'pending';
98
+ }
99
+ }
100
+
101
+ type OnCompleteCallback = (
102
+ completedTools: CompletedToolCall[],
103
+ ) => Promise<void> | void;
104
+ type OnUpdateCallback = (tools: TrackedToolCall[]) => void;
105
+
106
+ /**
107
+ * Update a single tool call with live output
108
+ */
109
+ function updateToolCallOutput(
110
+ call: TrackedToolCall,
111
+ toolCallId: string,
112
+ outputChunk: string,
113
+ ): TrackedToolCall {
114
+ if (call.request.callId !== toolCallId || call.status !== 'executing') {
115
+ return call;
116
+ }
117
+ return { ...call, liveOutput: outputChunk } as TrackedExecutingToolCall;
118
+ }
119
+
120
+ /**
121
+ * Apply output update to all tool calls
122
+ */
123
+ function applyOutputUpdate(
124
+ prevCalls: TrackedToolCall[],
125
+ toolCallId: string,
126
+ outputChunk: string,
127
+ ): TrackedToolCall[] {
128
+ // Check if any call would be updated
129
+ const hasMatch = prevCalls.some(
130
+ (call) => call.request.callId === toolCallId && call.status === 'executing',
131
+ );
132
+ if (!hasMatch) {
133
+ return prevCalls;
134
+ }
135
+ return prevCalls.map((call) =>
136
+ updateToolCallOutput(call, toolCallId, outputChunk),
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Transform core tool calls to tracked tool calls
142
+ */
143
+ function transformToTrackedCalls(
144
+ updatedCalls: CoreToolCall[],
145
+ prevCalls: TrackedToolCall[],
146
+ ): TrackedToolCall[] {
147
+ const previousCallMap = new Map(
148
+ prevCalls.map((call) => [call.request.callId, call]),
149
+ );
150
+ return updatedCalls.map((call) => ({
151
+ ...call,
152
+ responseSubmittedToModel:
153
+ previousCallMap.get(call.request.callId)?.responseSubmittedToModel ??
154
+ false,
155
+ })) as TrackedToolCall[];
156
+ }
157
+
158
+ /**
159
+ * Get description from a tool call
160
+ */
161
+ function getToolCallDescription(call: TrackedToolCall): string {
162
+ if ('invocation' in call) {
163
+ const invocation = call.invocation as
164
+ | { getDescription(): string }
165
+ | undefined;
166
+ if (invocation) {
167
+ return invocation.getDescription();
168
+ }
169
+ }
170
+ return JSON.stringify(call.request.args);
171
+ }
172
+
173
+ /**
174
+ * Get output from a completed tool call
175
+ */
176
+ function getToolCallOutput(call: TrackedToolCall): string | undefined {
177
+ if (
178
+ call.status === 'success' ||
179
+ call.status === 'error' ||
180
+ call.status === 'cancelled'
181
+ ) {
182
+ const completed = call as CompletedToolCall | CancelledToolCall;
183
+ if (completed.response.resultDisplay != null) {
184
+ return typeof completed.response.resultDisplay === 'string'
185
+ ? completed.response.resultDisplay
186
+ : JSON.stringify(completed.response.resultDisplay);
187
+ }
188
+ }
189
+ if (call.status === 'executing') {
190
+ const executing = call as TrackedExecutingToolCall;
191
+ return executing.liveOutput;
192
+ }
193
+ return undefined;
194
+ }
195
+
196
+ /**
197
+ * Get error message from a failed tool call
198
+ */
199
+ function getToolCallError(call: TrackedToolCall): string | undefined {
200
+ if (call.status === 'error') {
201
+ const completed = call as CompletedToolCall;
202
+ const error = completed.response.error;
203
+ if (error) {
204
+ return error.message;
205
+ }
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ /**
211
+ * Convert a TrackedToolCall to ToolCallDisplayInfo
212
+ */
213
+ function toDisplayInfo(call: TrackedToolCall): ToolCallDisplayInfo {
214
+ const displayName = call.tool?.displayName ?? call.request.name;
215
+ const description = getToolCallDescription(call);
216
+ const output = getToolCallOutput(call);
217
+ const errorMessage = getToolCallError(call);
218
+
219
+ const result: ToolCallDisplayInfo = {
220
+ callId: call.request.callId,
221
+ name: call.request.name,
222
+ displayName,
223
+ description,
224
+ status: mapCoreStatusToToolStatus(call.status),
225
+ };
226
+
227
+ if (output !== undefined) {
228
+ result.output = output;
229
+ }
230
+ if (errorMessage !== undefined) {
231
+ result.errorMessage = errorMessage;
232
+ }
233
+ if (call.status === 'awaiting_approval') {
234
+ const waiting = call as WaitingToolCall;
235
+ result.confirmationDetails = waiting.confirmationDetails;
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ /**
242
+ * Mark specified tool calls as submitted
243
+ */
244
+ function markCallsAsSubmitted(
245
+ prevCalls: TrackedToolCall[],
246
+ callIdsToMark: string[],
247
+ ): TrackedToolCall[] {
248
+ const hasMatch = prevCalls.some((call) =>
249
+ callIdsToMark.includes(call.request.callId),
250
+ );
251
+ if (!hasMatch) {
252
+ return prevCalls;
253
+ }
254
+ return prevCalls.map((call) => {
255
+ if (callIdsToMark.includes(call.request.callId)) {
256
+ return { ...call, responseSubmittedToModel: true };
257
+ }
258
+ return call;
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Hook that wraps CoreToolScheduler for React usage.
264
+ * Handles tool execution lifecycle with confirmation support.
265
+ */
266
+ export function useToolScheduler(
267
+ config: Config | null,
268
+ onAllToolCallsComplete: OnCompleteCallback,
269
+ onToolCallsUpdate?: OnUpdateCallback,
270
+ ): UseToolSchedulerResult {
271
+ const [toolCalls, setToolCalls] = useState<TrackedToolCall[]>([]);
272
+ const schedulerRef = useRef<CoreToolScheduler | null>(null);
273
+
274
+ // Use refs to store callbacks so they don't trigger effect re-runs
275
+ const onCompleteRef = useRef<OnCompleteCallback>(onAllToolCallsComplete);
276
+ const onUpdateRef = useRef<OnUpdateCallback | undefined>(onToolCallsUpdate);
277
+
278
+ // Keep refs in sync with props
279
+ useEffect(() => {
280
+ onCompleteRef.current = onAllToolCallsComplete;
281
+ }, [onAllToolCallsComplete]);
282
+
283
+ useEffect(() => {
284
+ onUpdateRef.current = onToolCallsUpdate;
285
+ }, [onToolCallsUpdate]);
286
+
287
+ // Create scheduler when config changes
288
+ useEffect(() => {
289
+ if (!config) {
290
+ schedulerRef.current = null;
291
+ return;
292
+ }
293
+
294
+ const handleOutputUpdate = (
295
+ toolCallId: string,
296
+ outputChunk: string,
297
+ ): void => {
298
+ setToolCalls((prev) => applyOutputUpdate(prev, toolCallId, outputChunk));
299
+ };
300
+
301
+ const handleToolCallsUpdate = (updatedCalls: CoreToolCall[]): void => {
302
+ setToolCalls((prevCalls) => {
303
+ if (updatedCalls.length === 0) {
304
+ return [];
305
+ }
306
+ const newCalls = transformToTrackedCalls(updatedCalls, prevCalls);
307
+ const updateCallback = onUpdateRef.current;
308
+ if (updateCallback) {
309
+ updateCallback(newCalls);
310
+ }
311
+ return newCalls;
312
+ });
313
+ };
314
+
315
+ const handleAllComplete = async (
316
+ completedToolCalls: CompletedToolCall[],
317
+ ): Promise<void> => {
318
+ logger.debug(
319
+ 'handleAllComplete called',
320
+ 'toolCount:',
321
+ completedToolCalls.length,
322
+ );
323
+ if (completedToolCalls.length > 0) {
324
+ logger.debug('handleAllComplete: calling onCompleteRef.current');
325
+ await onCompleteRef.current(completedToolCalls);
326
+ logger.debug('handleAllComplete: onCompleteRef.current returned');
327
+ } else {
328
+ logger.debug(
329
+ 'handleAllComplete: no completed tools, skipping callback',
330
+ );
331
+ }
332
+ handleToolCallsUpdate([]);
333
+ };
334
+
335
+ const scheduler = new CoreToolScheduler({
336
+ config,
337
+ outputUpdateHandler: handleOutputUpdate,
338
+ onAllToolCallsComplete: handleAllComplete,
339
+ onToolCallsUpdate: handleToolCallsUpdate,
340
+ getPreferredEditor: () => undefined,
341
+ onEditorClose: () => {
342
+ /* no-op */
343
+ },
344
+ });
345
+
346
+ schedulerRef.current = scheduler;
347
+
348
+ return () => {
349
+ schedulerRef.current = null;
350
+ };
351
+ }, [config]);
352
+
353
+ // Schedule new tool calls
354
+ const schedule: ScheduleFn = useCallback(
355
+ (
356
+ request: ToolCallRequestInfo | ToolCallRequestInfo[],
357
+ signal: AbortSignal,
358
+ ) => {
359
+ const scheduler = schedulerRef.current;
360
+ if (scheduler) {
361
+ void scheduler.schedule(request, signal);
362
+ }
363
+ },
364
+ [],
365
+ );
366
+
367
+ // Mark tools as submitted to the model
368
+ const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback(
369
+ (callIdsToMark: string[]) => {
370
+ if (callIdsToMark.length > 0) {
371
+ setToolCalls((prev) => markCallsAsSubmitted(prev, callIdsToMark));
372
+ }
373
+ },
374
+ [],
375
+ );
376
+
377
+ // Cancel all pending tool calls
378
+ const cancelAll: CancelAllFn = useCallback(() => {
379
+ const scheduler = schedulerRef.current;
380
+ if (scheduler !== null) {
381
+ // Cast needed as types may be out of sync with runtime
382
+ (scheduler as unknown as { cancelAll(): void }).cancelAll();
383
+ }
384
+ }, []);
385
+
386
+ // Get tool display info for UI rendering
387
+ const getToolDisplayInfo = useCallback((): ToolCallDisplayInfo[] => {
388
+ return toolCalls.map(toDisplayInfo);
389
+ }, [toolCalls]);
390
+
391
+ // Respond to a tool confirmation request
392
+ const respondToConfirmation: RespondToConfirmationFn = useCallback(
393
+ (callId: string, outcome: ToolConfirmationOutcome) => {
394
+ logger.debug(
395
+ 'respondToConfirmation called',
396
+ 'callId:',
397
+ callId,
398
+ 'outcome:',
399
+ outcome,
400
+ );
401
+
402
+ // Find the tool call with matching callId that is awaiting approval
403
+ const toolCall = toolCalls.find(
404
+ (tc) =>
405
+ tc.request.callId === callId && tc.status === 'awaiting_approval',
406
+ );
407
+
408
+ if (!toolCall) {
409
+ logger.warn(
410
+ 'respondToConfirmation: tool call not found or not awaiting approval',
411
+ 'callId:',
412
+ callId,
413
+ );
414
+ return;
415
+ }
416
+
417
+ const waitingCall = toolCall as WaitingToolCall;
418
+ logger.debug('Calling onConfirm callback', 'callId:', callId);
419
+ void waitingCall.confirmationDetails.onConfirm(outcome);
420
+ },
421
+ [toolCalls],
422
+ );
423
+
424
+ return {
425
+ toolCalls,
426
+ schedule,
427
+ markToolsAsSubmitted,
428
+ cancelAll,
429
+ getToolDisplayInfo,
430
+ respondToConfirmation,
431
+ };
432
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Public API for @vybestack/llxprt-ui package
2
+ export { startNui } from './bootstrap.js';
3
+ export type { UILaunchConfig } from './types.js';
package/src/jsx.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Override JSX namespace to allow ReactNode as Element
2
+ // This is needed because OpenTUI's JSX types are stricter than standard React
3
+ import type React from 'react';
4
+
5
+ declare global {
6
+ namespace JSX {
7
+ type Element = React.ReactNode;
8
+ }
9
+ }
10
+
11
+ export {};
@@ -0,0 +1,18 @@
1
+ import clipboardy from 'clipboardy';
2
+
3
+ export async function readClipboardText(): Promise<string | undefined> {
4
+ try {
5
+ const text = await clipboardy.read();
6
+ return text.trim().length > 0 ? text : undefined;
7
+ } catch {
8
+ return undefined;
9
+ }
10
+ }
11
+
12
+ export async function writeClipboardText(text: string): Promise<void> {
13
+ try {
14
+ await clipboardy.write(text);
15
+ } catch {
16
+ // ignore copy failures; OSC52 will still have been attempted
17
+ }
18
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Lightweight logger for nui.
3
+ * API compatible with @llxprt-code/core DebugLogger for future replacement.
4
+ * Currently logs to file, can be extended to use opentui's console capture.
5
+ */
6
+ import { appendFileSync, mkdirSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ const LOG_DIR = join(homedir(), '.llxprt', 'nuilog');
11
+ const LOG_FILE = join(LOG_DIR, 'nui.log');
12
+
13
+ // Ensure log directory exists
14
+ try {
15
+ mkdirSync(LOG_DIR, { recursive: true });
16
+ } catch {
17
+ // Ignore errors - logging is best-effort
18
+ }
19
+
20
+ type LogLevel = 'debug' | 'log' | 'warn' | 'error';
21
+
22
+ interface LogEntry {
23
+ timestamp: string;
24
+ namespace: string;
25
+ level: LogLevel;
26
+ message: string;
27
+ args?: unknown[];
28
+ }
29
+
30
+ function formatEntry(entry: LogEntry): string {
31
+ const argsStr =
32
+ entry.args !== undefined && entry.args.length > 0
33
+ ? ` ${JSON.stringify(entry.args)}`
34
+ : '';
35
+ return `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.namespace}] ${entry.message}${argsStr}`;
36
+ }
37
+
38
+ function writeLog(entry: LogEntry): void {
39
+ try {
40
+ appendFileSync(LOG_FILE, formatEntry(entry) + '\n', 'utf8');
41
+ } catch {
42
+ // Ignore write errors - logging is best-effort
43
+ }
44
+ }
45
+
46
+ export class Logger {
47
+ private _namespace: string;
48
+ private _enabled = true;
49
+
50
+ constructor(namespace: string) {
51
+ this._namespace = namespace;
52
+ }
53
+
54
+ get namespace(): string {
55
+ return this._namespace;
56
+ }
57
+
58
+ get enabled(): boolean {
59
+ return this._enabled;
60
+ }
61
+
62
+ set enabled(value: boolean) {
63
+ this._enabled = value;
64
+ }
65
+
66
+ private logAtLevel(level: LogLevel, message: string, args: unknown[]): void {
67
+ if (!this._enabled) return;
68
+
69
+ const entry: LogEntry = {
70
+ timestamp: new Date().toISOString(),
71
+ namespace: this._namespace,
72
+ level,
73
+ message,
74
+ args: args.length > 0 ? args : undefined,
75
+ };
76
+
77
+ writeLog(entry);
78
+ }
79
+
80
+ debug(message: string, ...args: unknown[]): void {
81
+ this.logAtLevel('debug', message, args);
82
+ }
83
+
84
+ log(message: string, ...args: unknown[]): void {
85
+ this.logAtLevel('log', message, args);
86
+ }
87
+
88
+ warn(message: string, ...args: unknown[]): void {
89
+ this.logAtLevel('warn', message, args);
90
+ }
91
+
92
+ error(message: string, ...args: unknown[]): void {
93
+ this.logAtLevel('error', message, args);
94
+ }
95
+ }
96
+
97
+ // Singleton loggers by namespace
98
+ const loggers = new Map<string, Logger>();
99
+
100
+ export function getLogger(namespace: string): Logger {
101
+ let logger = loggers.get(namespace);
102
+ if (logger === undefined) {
103
+ logger = new Logger(namespace);
104
+ loggers.set(namespace, logger);
105
+ }
106
+ return logger;
107
+ }
@@ -0,0 +1,5 @@
1
+ import { randomInt } from 'node:crypto';
2
+
3
+ export function secureRandomBetween(min: number, max: number): number {
4
+ return randomInt(min, max + 1);
5
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import { createCliRenderer } from '@vybestack/opentui-core';
2
+ import { createRoot } from '@vybestack/opentui-react';
3
+ import { App } from './app';
4
+
5
+ const renderer = await createCliRenderer({
6
+ exitOnCtrlC: true,
7
+ useMouse: true,
8
+ useAlternateScreen: true,
9
+ useKittyKeyboard: { events: true },
10
+ });
11
+
12
+ createRoot(renderer).render(<App />);
13
+ renderer.start();
@@ -0,0 +1,51 @@
1
+ import type { ThemeDefinition } from '../features/theme';
2
+
3
+ export function createMockTheme(): ThemeDefinition {
4
+ return {
5
+ slug: 'test',
6
+ name: 'Test Theme',
7
+ kind: 'dark',
8
+ colors: {
9
+ background: '#000000',
10
+ panel: {
11
+ bg: '#111111',
12
+ border: '#333333',
13
+ },
14
+ text: {
15
+ primary: '#ffffff',
16
+ muted: '#888888',
17
+ user: '#00ff00',
18
+ responder: '#0088ff',
19
+ thinking: '#ff8800',
20
+ tool: '#ff00ff',
21
+ },
22
+ input: {
23
+ fg: '#ffffff',
24
+ bg: '#000000',
25
+ border: '#333333',
26
+ placeholder: '#666666',
27
+ },
28
+ status: {
29
+ fg: '#ffffff',
30
+ },
31
+ accent: {
32
+ primary: '#00ffff',
33
+ },
34
+ diff: {
35
+ addedBg: '#003300',
36
+ addedFg: '#00ff00',
37
+ removedBg: '#330000',
38
+ removedFg: '#ff0000',
39
+ },
40
+ selection: {
41
+ fg: '#000000',
42
+ bg: '#ffffff',
43
+ },
44
+ message: {
45
+ userBorder: '#00ff00',
46
+ systemBorder: '#ffff00',
47
+ systemText: '#ffff00',
48
+ },
49
+ },
50
+ };
51
+ }