@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.
- package/PLAN-messages.md +681 -0
- package/PLAN.md +47 -0
- package/README.md +25 -0
- package/bun.lock +1024 -0
- package/dev-docs/ARCHITECTURE.md +178 -0
- package/dev-docs/CODE_ORGANIZATION.md +232 -0
- package/dev-docs/STANDARDS.md +235 -0
- package/dev-docs/UI_DESIGN.md +425 -0
- package/eslint.config.cjs +194 -0
- package/images/nui.png +0 -0
- package/llxprt.png +0 -0
- package/llxprt.svg +128 -0
- package/package.json +66 -0
- package/scripts/check-limits.ts +177 -0
- package/scripts/start.js +71 -0
- package/src/app.tsx +599 -0
- package/src/bootstrap.tsx +23 -0
- package/src/commands/AuthCommand.tsx +80 -0
- package/src/commands/ModelCommand.tsx +102 -0
- package/src/commands/ProviderCommand.tsx +103 -0
- package/src/commands/ThemeCommand.tsx +71 -0
- package/src/features/chat/history.ts +178 -0
- package/src/features/chat/index.ts +3 -0
- package/src/features/chat/persistentHistory.ts +102 -0
- package/src/features/chat/responder.ts +217 -0
- package/src/features/completion/completions.ts +161 -0
- package/src/features/completion/index.ts +3 -0
- package/src/features/completion/slash.test.ts +82 -0
- package/src/features/completion/slash.ts +248 -0
- package/src/features/completion/suggestions.test.ts +51 -0
- package/src/features/completion/suggestions.ts +112 -0
- package/src/features/config/configSession.test.ts +189 -0
- package/src/features/config/configSession.ts +179 -0
- package/src/features/config/index.ts +4 -0
- package/src/features/config/llxprtAdapter.integration.test.ts +202 -0
- package/src/features/config/llxprtAdapter.test.ts +139 -0
- package/src/features/config/llxprtAdapter.ts +257 -0
- package/src/features/config/llxprtCommands.test.ts +40 -0
- package/src/features/config/llxprtCommands.ts +35 -0
- package/src/features/config/llxprtConfig.test.ts +261 -0
- package/src/features/config/llxprtConfig.ts +418 -0
- package/src/features/theme/index.ts +2 -0
- package/src/features/theme/theme.test.ts +51 -0
- package/src/features/theme/theme.ts +105 -0
- package/src/features/theme/themeManager.ts +84 -0
- package/src/hooks/useAppCommands.ts +129 -0
- package/src/hooks/useApprovalKeyboard.ts +156 -0
- package/src/hooks/useChatStore.test.ts +112 -0
- package/src/hooks/useChatStore.ts +252 -0
- package/src/hooks/useInputManager.ts +99 -0
- package/src/hooks/useKeyboardHandlers.ts +130 -0
- package/src/hooks/useListNavigation.test.ts +166 -0
- package/src/hooks/useListNavigation.ts +62 -0
- package/src/hooks/usePersistentHistory.ts +94 -0
- package/src/hooks/useScrollManagement.ts +107 -0
- package/src/hooks/useSelectionClipboard.ts +48 -0
- package/src/hooks/useSessionManager.test.ts +85 -0
- package/src/hooks/useSessionManager.ts +101 -0
- package/src/hooks/useStreamingLifecycle.ts +71 -0
- package/src/hooks/useStreamingResponder.ts +401 -0
- package/src/hooks/useSuggestionSetup.ts +23 -0
- package/src/hooks/useToolApproval.test.ts +140 -0
- package/src/hooks/useToolApproval.ts +264 -0
- package/src/hooks/useToolScheduler.ts +432 -0
- package/src/index.ts +3 -0
- package/src/jsx.d.ts +11 -0
- package/src/lib/clipboard.ts +18 -0
- package/src/lib/logger.ts +107 -0
- package/src/lib/random.ts +5 -0
- package/src/main.tsx +13 -0
- package/src/test/mockTheme.ts +51 -0
- package/src/types/events.ts +87 -0
- package/src/types.ts +13 -0
- package/src/ui/components/ChatLayout.tsx +694 -0
- package/src/ui/components/CommandComponents.tsx +74 -0
- package/src/ui/components/DiffViewer.tsx +306 -0
- package/src/ui/components/FilterInput.test.ts +69 -0
- package/src/ui/components/FilterInput.tsx +62 -0
- package/src/ui/components/HeaderBar.tsx +137 -0
- package/src/ui/components/RadioSelect.test.ts +140 -0
- package/src/ui/components/RadioSelect.tsx +88 -0
- package/src/ui/components/SelectableList.test.ts +83 -0
- package/src/ui/components/SelectableList.tsx +35 -0
- package/src/ui/components/StatusBar.tsx +45 -0
- package/src/ui/components/SuggestionPanel.tsx +102 -0
- package/src/ui/components/messages/ModelMessage.tsx +14 -0
- package/src/ui/components/messages/SystemMessage.tsx +29 -0
- package/src/ui/components/messages/ThinkingMessage.tsx +14 -0
- package/src/ui/components/messages/UserMessage.tsx +26 -0
- package/src/ui/components/messages/index.ts +15 -0
- package/src/ui/components/messages/renderMessage.test.ts +49 -0
- package/src/ui/components/messages/renderMessage.tsx +43 -0
- package/src/ui/components/messages/types.test.ts +24 -0
- package/src/ui/components/messages/types.ts +36 -0
- package/src/ui/modals/AuthModal.tsx +106 -0
- package/src/ui/modals/ModalShell.tsx +60 -0
- package/src/ui/modals/SearchSelectModal.tsx +236 -0
- package/src/ui/modals/ThemeModal.tsx +204 -0
- package/src/ui/modals/ToolApprovalModal.test.ts +206 -0
- package/src/ui/modals/ToolApprovalModal.tsx +282 -0
- package/src/ui/modals/index.ts +20 -0
- package/src/ui/modals/modals.test.ts +26 -0
- package/src/ui/modals/types.ts +19 -0
- package/src/uicontext/Command.tsx +102 -0
- package/src/uicontext/Dialog.tsx +65 -0
- package/src/uicontext/index.ts +2 -0
- package/themes/ansi-light.json +59 -0
- package/themes/ansi.json +59 -0
- package/themes/atom-one-dark.json +59 -0
- package/themes/ayu-light.json +59 -0
- package/themes/ayu.json +59 -0
- package/themes/default-light.json +59 -0
- package/themes/default.json +59 -0
- package/themes/dracula.json +59 -0
- package/themes/github-dark.json +59 -0
- package/themes/github-light.json +59 -0
- package/themes/googlecode.json +59 -0
- package/themes/green-screen.json +59 -0
- package/themes/no-color.json +59 -0
- package/themes/shades-of-purple.json +59 -0
- package/themes/xcode.json +59 -0
- package/tsconfig.json +28 -0
- 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
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
|
+
}
|
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
|
+
}
|