@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,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { PendingApproval, UseToolApprovalResult } from './useToolApproval';
3
+ import type { ToolConfirmationType } from '../types/events';
4
+
5
+ describe('useToolApproval', () => {
6
+ describe('PendingApproval type', () => {
7
+ it('extends ToolApprovalDetails with correlationId', () => {
8
+ const approval: PendingApproval = {
9
+ callId: 'call-123',
10
+ toolName: 'write_file',
11
+ confirmationType: 'edit' as ToolConfirmationType,
12
+ question: 'Allow file write?',
13
+ preview: 'Writing to /path/to/file.ts',
14
+ params: { path: '/path/to/file.ts' },
15
+ canAllowAlways: true,
16
+ correlationId: 'corr-456',
17
+ };
18
+
19
+ expect(approval.callId).toBe('call-123');
20
+ expect(approval.correlationId).toBe('corr-456');
21
+ expect(approval.toolName).toBe('write_file');
22
+ });
23
+
24
+ it('supports all confirmation types', () => {
25
+ const confirmationTypes: ToolConfirmationType[] = [
26
+ 'edit',
27
+ 'exec',
28
+ 'mcp',
29
+ 'info',
30
+ ];
31
+
32
+ for (const type of confirmationTypes) {
33
+ const approval: PendingApproval = {
34
+ callId: `call-${type}`,
35
+ toolName: `${type}_tool`,
36
+ confirmationType: type,
37
+ question: `Allow ${type}?`,
38
+ preview: `Preview for ${type}`,
39
+ params: {},
40
+ canAllowAlways: true,
41
+ correlationId: `corr-${type}`,
42
+ };
43
+
44
+ expect(approval.confirmationType).toBe(type);
45
+ }
46
+ });
47
+ });
48
+
49
+ describe('UseToolApprovalResult type', () => {
50
+ it('has correct shape', () => {
51
+ const mockResult: UseToolApprovalResult = {
52
+ pendingApproval: null,
53
+ queueApproval: () => {},
54
+ queueApprovalFromScheduler: () => {},
55
+ handleDecision: () => {},
56
+ clearApproval: () => {},
57
+ };
58
+
59
+ expect(mockResult.pendingApproval).toBeNull();
60
+ expect(typeof mockResult.queueApproval).toBe('function');
61
+ expect(typeof mockResult.queueApprovalFromScheduler).toBe('function');
62
+ expect(typeof mockResult.handleDecision).toBe('function');
63
+ expect(typeof mockResult.clearApproval).toBe('function');
64
+ });
65
+
66
+ it('pendingApproval can be a PendingApproval object', () => {
67
+ const approval: PendingApproval = {
68
+ callId: 'call-789',
69
+ toolName: 'run_command',
70
+ confirmationType: 'exec' as ToolConfirmationType,
71
+ question: 'Allow command?',
72
+ preview: 'npm test',
73
+ params: { command: 'npm test' },
74
+ canAllowAlways: false,
75
+ correlationId: 'corr-789',
76
+ };
77
+
78
+ const mockResult: UseToolApprovalResult = {
79
+ pendingApproval: approval,
80
+ queueApproval: () => {},
81
+ queueApprovalFromScheduler: () => {},
82
+ handleDecision: () => {},
83
+ clearApproval: () => {},
84
+ };
85
+
86
+ expect(mockResult.pendingApproval).toBe(approval);
87
+ expect(mockResult.pendingApproval?.toolName).toBe('run_command');
88
+ });
89
+
90
+ it('queueApproval accepts PendingApproval', () => {
91
+ let queuedApproval: PendingApproval | null = null;
92
+
93
+ const mockResult: UseToolApprovalResult = {
94
+ pendingApproval: null,
95
+ queueApproval: (approval: PendingApproval) => {
96
+ queuedApproval = approval;
97
+ },
98
+ queueApprovalFromScheduler: () => {},
99
+ handleDecision: () => {},
100
+ clearApproval: () => {},
101
+ };
102
+
103
+ const approval: PendingApproval = {
104
+ callId: 'call-queue',
105
+ toolName: 'test_tool',
106
+ confirmationType: 'info' as ToolConfirmationType,
107
+ question: 'Allow?',
108
+ preview: 'Test',
109
+ params: {},
110
+ canAllowAlways: true,
111
+ correlationId: 'corr-queue',
112
+ };
113
+
114
+ mockResult.queueApproval(approval);
115
+
116
+ expect(queuedApproval).toBe(approval);
117
+ });
118
+
119
+ it('handleDecision receives callId and outcome', () => {
120
+ let receivedCallId: string | null = null;
121
+ let receivedOutcome: string | null = null;
122
+
123
+ const mockResult: UseToolApprovalResult = {
124
+ pendingApproval: null,
125
+ queueApproval: () => {},
126
+ queueApprovalFromScheduler: () => {},
127
+ handleDecision: (callId: string, outcome) => {
128
+ receivedCallId = callId;
129
+ receivedOutcome = outcome;
130
+ },
131
+ clearApproval: () => {},
132
+ };
133
+
134
+ mockResult.handleDecision('call-decision', 'allow_always');
135
+
136
+ expect(receivedCallId).toBe('call-decision');
137
+ expect(receivedOutcome).toBe('allow_always');
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,264 @@
1
+ import { useCallback, useState, useRef, useEffect } from 'react';
2
+ import type {
3
+ ToolApprovalDetails,
4
+ ToolApprovalOutcome,
5
+ } from '../ui/modals/ToolApprovalModal';
6
+ import type { ToolCallConfirmationDetails } from '@vybestack/llxprt-code-core';
7
+ import { ToolConfirmationOutcome } from '@vybestack/llxprt-code-core';
8
+ import type { ToolConfirmationType } from '../types/events';
9
+ import { getLogger } from '../lib/logger';
10
+
11
+ const logger = getLogger('nui:tool-approval');
12
+
13
+ /**
14
+ * Function type for responding to tool confirmations
15
+ */
16
+ export type RespondToConfirmationFn = (
17
+ callId: string,
18
+ outcome: ToolConfirmationOutcome,
19
+ ) => void;
20
+
21
+ export interface PendingApproval extends ToolApprovalDetails {
22
+ readonly correlationId: string;
23
+ }
24
+
25
+ export interface UseToolApprovalResult {
26
+ readonly pendingApproval: PendingApproval | null;
27
+ readonly queueApproval: (approval: PendingApproval) => void;
28
+ readonly queueApprovalFromScheduler: (
29
+ callId: string,
30
+ toolName: string,
31
+ confirmationDetails: ToolCallConfirmationDetails,
32
+ ) => void;
33
+ readonly handleDecision: (
34
+ callId: string,
35
+ outcome: ToolApprovalOutcome,
36
+ ) => void;
37
+ readonly clearApproval: () => void;
38
+ }
39
+
40
+ function mapOutcome(outcome: ToolApprovalOutcome): ToolConfirmationOutcome {
41
+ switch (outcome) {
42
+ case 'allow_once':
43
+ return ToolConfirmationOutcome.ProceedOnce;
44
+ case 'allow_always':
45
+ return ToolConfirmationOutcome.ProceedAlways;
46
+ case 'cancel':
47
+ return ToolConfirmationOutcome.Cancel;
48
+ default:
49
+ return ToolConfirmationOutcome.Cancel;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Map CoreToolScheduler confirmation type to UI confirmation type
55
+ */
56
+ function mapConfirmationType(type: string): ToolConfirmationType {
57
+ switch (type) {
58
+ case 'edit':
59
+ return 'edit';
60
+ case 'exec':
61
+ return 'exec';
62
+ case 'mcp':
63
+ return 'mcp';
64
+ case 'info':
65
+ return 'info';
66
+ default:
67
+ return 'info';
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get question based on confirmation type
73
+ */
74
+ function getQuestionForType(details: ToolCallConfirmationDetails): string {
75
+ switch (details.type) {
76
+ case 'edit':
77
+ return 'Apply this change?';
78
+ case 'exec':
79
+ return `Allow execution of: '${details.rootCommand}'?`;
80
+ case 'mcp':
81
+ return `Allow execution of MCP tool "${details.toolName}" from server "${details.serverName}"?`;
82
+ case 'info':
83
+ return 'Do you want to proceed?';
84
+ default:
85
+ return 'Do you want to proceed?';
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get preview string from confirmation details
91
+ */
92
+ function getPreviewForType(details: ToolCallConfirmationDetails): string {
93
+ switch (details.type) {
94
+ case 'edit':
95
+ return details.fileDiff || `Edit: ${details.filePath}`;
96
+ case 'exec':
97
+ return details.command;
98
+ case 'mcp':
99
+ return `MCP Server: ${details.serverName}\nTool: ${details.toolDisplayName}`;
100
+ case 'info':
101
+ return details.prompt || '';
102
+ default:
103
+ return '';
104
+ }
105
+ }
106
+
107
+ export function useToolApproval(
108
+ respondToConfirmation: RespondToConfirmationFn | null,
109
+ ): UseToolApprovalResult {
110
+ const [pendingApproval, setPendingApproval] =
111
+ useState<PendingApproval | null>(null);
112
+ const approvalQueueRef = useRef<PendingApproval[]>([]);
113
+
114
+ // Ref to track current pendingApproval to avoid stale closures
115
+ const pendingApprovalRef = useRef<PendingApproval | null>(null);
116
+ useEffect(() => {
117
+ pendingApprovalRef.current = pendingApproval;
118
+ }, [pendingApproval]);
119
+
120
+ // Ref to track respondToConfirmation to avoid stale closures
121
+ const respondToConfirmationRef = useRef<RespondToConfirmationFn | null>(
122
+ respondToConfirmation,
123
+ );
124
+ useEffect(() => {
125
+ respondToConfirmationRef.current = respondToConfirmation;
126
+ }, [respondToConfirmation]);
127
+
128
+ const processNextApproval = useCallback(() => {
129
+ if (approvalQueueRef.current.length > 0) {
130
+ const next = approvalQueueRef.current.shift();
131
+ if (next) {
132
+ setPendingApproval(next);
133
+ }
134
+ } else {
135
+ setPendingApproval(null);
136
+ }
137
+ }, []);
138
+
139
+ const queueApproval = useCallback(
140
+ (approval: PendingApproval) => {
141
+ approvalQueueRef.current.push(approval);
142
+ // If no current pending approval, show this one
143
+ if (pendingApproval === null) {
144
+ processNextApproval();
145
+ }
146
+ },
147
+ [pendingApproval, processNextApproval],
148
+ );
149
+
150
+ /**
151
+ * Queue approval from CoreToolScheduler's waiting tool call
152
+ */
153
+ const queueApprovalFromScheduler = useCallback(
154
+ (
155
+ callId: string,
156
+ toolName: string,
157
+ confirmationDetails: ToolCallConfirmationDetails,
158
+ ) => {
159
+ const approval: PendingApproval = {
160
+ callId,
161
+ toolName,
162
+ confirmationType: mapConfirmationType(confirmationDetails.type),
163
+ question: getQuestionForType(confirmationDetails),
164
+ preview: getPreviewForType(confirmationDetails),
165
+ params: {}, // Params are embedded in confirmationDetails
166
+ canAllowAlways: true, // Can be refined based on policy
167
+ correlationId: String(
168
+ (confirmationDetails as { correlationId?: string }).correlationId ??
169
+ callId,
170
+ ),
171
+ coreDetails: confirmationDetails,
172
+ };
173
+ queueApproval(approval);
174
+ },
175
+ [queueApproval],
176
+ );
177
+
178
+ const handleDecision = useCallback(
179
+ (callId: string, outcome: ToolApprovalOutcome) => {
180
+ const currentApproval = pendingApprovalRef.current;
181
+ const confirmFn = respondToConfirmationRef.current;
182
+ logger.debug(
183
+ 'handleDecision called',
184
+ 'callId:',
185
+ callId,
186
+ 'outcome:',
187
+ outcome,
188
+ 'currentApproval:',
189
+ currentApproval?.callId,
190
+ 'hasConfirmFn:',
191
+ !!confirmFn,
192
+ );
193
+
194
+ if (!confirmFn) {
195
+ logger.warn('handleDecision: no respondToConfirmation function');
196
+ return;
197
+ }
198
+ if (!currentApproval) {
199
+ logger.warn('handleDecision: no currentApproval');
200
+ return;
201
+ }
202
+ if (currentApproval.callId !== callId) {
203
+ logger.warn(
204
+ 'handleDecision: callId mismatch',
205
+ 'expected:',
206
+ currentApproval.callId,
207
+ 'got:',
208
+ callId,
209
+ );
210
+ return;
211
+ }
212
+
213
+ try {
214
+ const coreOutcome = mapOutcome(outcome);
215
+ logger.debug(
216
+ 'Calling respondToConfirmation',
217
+ 'callId:',
218
+ callId,
219
+ 'outcome:',
220
+ coreOutcome,
221
+ );
222
+ confirmFn(callId, coreOutcome);
223
+ logger.debug('respondToConfirmation called successfully');
224
+
225
+ // Move to next approval in queue
226
+ processNextApproval();
227
+ } catch (err) {
228
+ logger.error('Error in handleDecision:', String(err));
229
+ }
230
+ },
231
+ [processNextApproval],
232
+ );
233
+
234
+ const clearApproval = useCallback(() => {
235
+ // Cancel all pending approvals
236
+ const currentApproval = pendingApprovalRef.current;
237
+ const confirmFn = respondToConfirmationRef.current;
238
+ if (confirmFn && currentApproval) {
239
+ logger.debug(
240
+ 'clearApproval: cancelling',
241
+ 'callId:',
242
+ currentApproval.callId,
243
+ );
244
+ confirmFn(currentApproval.callId, ToolConfirmationOutcome.Cancel);
245
+ }
246
+ approvalQueueRef.current = [];
247
+ setPendingApproval(null);
248
+ }, []);
249
+
250
+ // Clean up on unmount
251
+ useEffect(() => {
252
+ return () => {
253
+ approvalQueueRef.current = [];
254
+ };
255
+ }, []);
256
+
257
+ return {
258
+ pendingApproval,
259
+ queueApproval,
260
+ queueApprovalFromScheduler,
261
+ handleDecision,
262
+ clearApproval,
263
+ };
264
+ }