@ebowwa/coder 0.7.64 → 0.7.65
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/index.js +36168 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +283 -173
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
|
@@ -1,584 +0,0 @@
|
|
|
1
|
-
/** @jsx React.createElement */
|
|
2
|
-
/**
|
|
3
|
-
* Interactive Elements for TUI
|
|
4
|
-
*
|
|
5
|
-
* Components:
|
|
6
|
-
* - Toast: Notification messages that auto-dismiss
|
|
7
|
-
* - Modal: Dialog overlay for confirmations
|
|
8
|
-
* - SelectableList: Keyboard-navigable list with selection
|
|
9
|
-
* - Link: Clickable OSC 8 hyperlinks
|
|
10
|
-
*
|
|
11
|
-
* NOTE: All components use InputContext for keyboard input.
|
|
12
|
-
* Do NOT use process.stdin directly - it conflicts with the main input loop.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import React, { useState, useEffect, useCallback, useRef, createContext, useContext } from "react";
|
|
16
|
-
import { Box, Text, useStdout } from "ink";
|
|
17
|
-
import { useInputHandler, InputPriority, type NativeKeyEvent } from "../InputContext.js";
|
|
18
|
-
|
|
19
|
-
// ============================================
|
|
20
|
-
// TOAST COMPONENT
|
|
21
|
-
// ============================================
|
|
22
|
-
|
|
23
|
-
export type ToastType = "info" | "success" | "warning" | "error";
|
|
24
|
-
|
|
25
|
-
export interface ToastMessage {
|
|
26
|
-
id: string;
|
|
27
|
-
type: ToastType;
|
|
28
|
-
message: string;
|
|
29
|
-
duration?: number; // ms, 0 = no auto-dismiss
|
|
30
|
-
timestamp: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ToastProps {
|
|
34
|
-
toasts: ToastMessage[];
|
|
35
|
-
position?: "top" | "bottom";
|
|
36
|
-
maxVisible?: number;
|
|
37
|
-
onDismiss?: (id: string) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const TOAST_ICONS: Record<ToastType, string> = {
|
|
41
|
-
info: "i",
|
|
42
|
-
success: "+",
|
|
43
|
-
warning: "!",
|
|
44
|
-
error: "x",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const TOAST_COLORS: Record<ToastType, string> = {
|
|
48
|
-
info: "blue",
|
|
49
|
-
success: "green",
|
|
50
|
-
warning: "yellow",
|
|
51
|
-
error: "red",
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export function Toast({ toasts, position = "top", maxVisible = 3, onDismiss }: ToastProps) {
|
|
55
|
-
const visibleToasts = toasts.slice(0, maxVisible);
|
|
56
|
-
|
|
57
|
-
if (visibleToasts.length === 0) return null;
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<Box
|
|
61
|
-
flexDirection="column"
|
|
62
|
-
width="100%"
|
|
63
|
-
paddingX={1}
|
|
64
|
-
>
|
|
65
|
-
{visibleToasts.map((toast) => (
|
|
66
|
-
<Box
|
|
67
|
-
key={toast.id}
|
|
68
|
-
borderStyle="round"
|
|
69
|
-
borderColor={TOAST_COLORS[toast.type]}
|
|
70
|
-
paddingX={1}
|
|
71
|
-
marginBottom={1}
|
|
72
|
-
>
|
|
73
|
-
<Text color={TOAST_COLORS[toast.type]} bold>
|
|
74
|
-
[{TOAST_ICONS[toast.type]}]
|
|
75
|
-
</Text>
|
|
76
|
-
<Text> {toast.message}</Text>
|
|
77
|
-
</Box>
|
|
78
|
-
))}
|
|
79
|
-
</Box>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Toast Manager Hook
|
|
84
|
-
export interface ToastManager {
|
|
85
|
-
toasts: ToastMessage[];
|
|
86
|
-
showToast: (type: ToastType, message: string, duration?: number) => string;
|
|
87
|
-
dismissToast: (id: string) => void;
|
|
88
|
-
clearAllToasts: () => void;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function useToast(defaultDuration = 5000): ToastManager {
|
|
92
|
-
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
93
|
-
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
94
|
-
|
|
95
|
-
const showToast = useCallback((type: ToastType, message: string, duration = defaultDuration): string => {
|
|
96
|
-
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
97
|
-
const toast: ToastMessage = {
|
|
98
|
-
id,
|
|
99
|
-
type,
|
|
100
|
-
message,
|
|
101
|
-
duration,
|
|
102
|
-
timestamp: Date.now(),
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
setToasts((prev) => [...prev, toast]);
|
|
106
|
-
|
|
107
|
-
// Auto-dismiss
|
|
108
|
-
if (duration > 0) {
|
|
109
|
-
const timer = setTimeout(() => {
|
|
110
|
-
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
111
|
-
timersRef.current.delete(id);
|
|
112
|
-
}, duration);
|
|
113
|
-
timersRef.current.set(id, timer);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return id;
|
|
117
|
-
}, [defaultDuration]);
|
|
118
|
-
|
|
119
|
-
const dismissToast = useCallback((id: string) => {
|
|
120
|
-
const timer = timersRef.current.get(id);
|
|
121
|
-
if (timer) {
|
|
122
|
-
clearTimeout(timer);
|
|
123
|
-
timersRef.current.delete(id);
|
|
124
|
-
}
|
|
125
|
-
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
const clearAllToasts = useCallback(() => {
|
|
129
|
-
timersRef.current.forEach((timer) => clearTimeout(timer));
|
|
130
|
-
timersRef.current.clear();
|
|
131
|
-
setToasts([]);
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
|
-
// Cleanup on unmount
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
return () => {
|
|
137
|
-
timersRef.current.forEach((timer) => clearTimeout(timer));
|
|
138
|
-
};
|
|
139
|
-
}, []);
|
|
140
|
-
|
|
141
|
-
return { toasts, showToast, dismissToast, clearAllToasts };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ============================================
|
|
145
|
-
// MODAL COMPONENT
|
|
146
|
-
// ============================================
|
|
147
|
-
|
|
148
|
-
export interface ModalAction {
|
|
149
|
-
id: string;
|
|
150
|
-
label: string;
|
|
151
|
-
style?: "primary" | "secondary" | "danger";
|
|
152
|
-
onActivate?: () => void | Promise<void>;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface ModalProps {
|
|
156
|
-
isOpen: boolean;
|
|
157
|
-
title?: string;
|
|
158
|
-
message: string;
|
|
159
|
-
actions: ModalAction[];
|
|
160
|
-
defaultAction?: string;
|
|
161
|
-
onDismiss?: () => void;
|
|
162
|
-
width?: number;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ACTION_COLORS: Record<string, string> = {
|
|
166
|
-
primary: "cyan",
|
|
167
|
-
secondary: "gray",
|
|
168
|
-
danger: "red",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
export function Modal({
|
|
172
|
-
isOpen,
|
|
173
|
-
title,
|
|
174
|
-
message,
|
|
175
|
-
actions,
|
|
176
|
-
defaultAction,
|
|
177
|
-
onDismiss,
|
|
178
|
-
width = 60,
|
|
179
|
-
}: ModalProps) {
|
|
180
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
181
|
-
const { stdout } = useStdout();
|
|
182
|
-
|
|
183
|
-
useEffect(() => {
|
|
184
|
-
// Reset selection when modal opens
|
|
185
|
-
if (isOpen) {
|
|
186
|
-
const defaultIndex = defaultAction
|
|
187
|
-
? actions.findIndex((a) => a.id === defaultAction)
|
|
188
|
-
: 0;
|
|
189
|
-
setSelectedIndex(defaultIndex >= 0 ? defaultIndex : 0);
|
|
190
|
-
}
|
|
191
|
-
}, [isOpen, defaultAction, actions]);
|
|
192
|
-
|
|
193
|
-
// Handle keyboard input via centralized InputContext
|
|
194
|
-
const handleKey = useCallback((event: NativeKeyEvent): boolean => {
|
|
195
|
-
if (!isOpen) return false;
|
|
196
|
-
|
|
197
|
-
// Escape or q to dismiss
|
|
198
|
-
if ((event.code === "escape" && event.kind === "press") ||
|
|
199
|
-
(event.code === "q" && !event.ctrl && event.kind === "press")) {
|
|
200
|
-
onDismiss?.();
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Enter to select
|
|
205
|
-
if (event.code === "enter" && event.kind === "press") {
|
|
206
|
-
const action = actions[selectedIndex];
|
|
207
|
-
if (action?.onActivate) {
|
|
208
|
-
action.onActivate();
|
|
209
|
-
}
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Left arrow or h
|
|
214
|
-
if ((event.code === "left" && event.kind === "press") ||
|
|
215
|
-
(event.code === "h" && !event.ctrl && event.kind === "press")) {
|
|
216
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Right arrow or l
|
|
221
|
-
if ((event.code === "right" && event.kind === "press") ||
|
|
222
|
-
(event.code === "l" && !event.ctrl && event.kind === "press")) {
|
|
223
|
-
setSelectedIndex((prev) => Math.min(actions.length - 1, prev + 1));
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return false;
|
|
228
|
-
}, [isOpen, actions, selectedIndex, onDismiss]);
|
|
229
|
-
|
|
230
|
-
// Register with input system - modals have high priority
|
|
231
|
-
useInputHandler("modal", handleKey, {
|
|
232
|
-
priority: InputPriority.MODAL,
|
|
233
|
-
isActive: isOpen,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (!isOpen) return null;
|
|
237
|
-
|
|
238
|
-
const modalWidth = Math.min(width, stdout.columns || 80);
|
|
239
|
-
|
|
240
|
-
return (
|
|
241
|
-
<Box
|
|
242
|
-
flexDirection="column"
|
|
243
|
-
borderStyle="double"
|
|
244
|
-
borderColor="cyan"
|
|
245
|
-
width={modalWidth}
|
|
246
|
-
paddingX={2}
|
|
247
|
-
paddingY={1}
|
|
248
|
-
>
|
|
249
|
-
{/* Title */}
|
|
250
|
-
{title && (
|
|
251
|
-
<Box>
|
|
252
|
-
<Text bold color="cyan">{title}</Text>
|
|
253
|
-
</Box>
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{/* Message */}
|
|
257
|
-
<Box marginBottom={1}>
|
|
258
|
-
<Text>{message}</Text>
|
|
259
|
-
</Box>
|
|
260
|
-
|
|
261
|
-
{/* Actions */}
|
|
262
|
-
<Box justifyContent="center">
|
|
263
|
-
{actions.map((action, index) => {
|
|
264
|
-
const isSelected = index === selectedIndex;
|
|
265
|
-
const color = ACTION_COLORS[action.style || "secondary"];
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<Box
|
|
269
|
-
key={action.id}
|
|
270
|
-
borderStyle={isSelected ? "single" : undefined}
|
|
271
|
-
borderColor={isSelected ? color : undefined}
|
|
272
|
-
paddingX={1}
|
|
273
|
-
marginRight={1}
|
|
274
|
-
>
|
|
275
|
-
<Text
|
|
276
|
-
color={color}
|
|
277
|
-
bold={isSelected}
|
|
278
|
-
inverse={isSelected}
|
|
279
|
-
>
|
|
280
|
-
{action.label}
|
|
281
|
-
</Text>
|
|
282
|
-
</Box>
|
|
283
|
-
);
|
|
284
|
-
})}
|
|
285
|
-
</Box>
|
|
286
|
-
|
|
287
|
-
{/* Hint */}
|
|
288
|
-
<Box marginTop={1}>
|
|
289
|
-
<Text dimColor>
|
|
290
|
-
{"<-"}/{"->"} or h/l to select | Enter to confirm | Esc to cancel
|
|
291
|
-
</Text>
|
|
292
|
-
</Box>
|
|
293
|
-
</Box>
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Confirmation Modal Helper
|
|
298
|
-
export interface ConfirmModalProps {
|
|
299
|
-
isOpen: boolean;
|
|
300
|
-
title?: string;
|
|
301
|
-
message: string;
|
|
302
|
-
onConfirm: () => void | Promise<void>;
|
|
303
|
-
onCancel: () => void;
|
|
304
|
-
confirmLabel?: string;
|
|
305
|
-
cancelLabel?: string;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function ConfirmModal({
|
|
309
|
-
isOpen,
|
|
310
|
-
title = "Confirm",
|
|
311
|
-
message,
|
|
312
|
-
onConfirm,
|
|
313
|
-
onCancel,
|
|
314
|
-
confirmLabel = "Confirm",
|
|
315
|
-
cancelLabel = "Cancel",
|
|
316
|
-
}: ConfirmModalProps) {
|
|
317
|
-
return (
|
|
318
|
-
<Modal
|
|
319
|
-
isOpen={isOpen}
|
|
320
|
-
title={title}
|
|
321
|
-
message={message}
|
|
322
|
-
actions={[
|
|
323
|
-
{
|
|
324
|
-
id: "cancel",
|
|
325
|
-
label: cancelLabel,
|
|
326
|
-
style: "secondary",
|
|
327
|
-
onActivate: onCancel,
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
id: "confirm",
|
|
331
|
-
label: confirmLabel,
|
|
332
|
-
style: "primary",
|
|
333
|
-
onActivate: onConfirm,
|
|
334
|
-
},
|
|
335
|
-
]}
|
|
336
|
-
defaultAction="confirm"
|
|
337
|
-
onDismiss={onCancel}
|
|
338
|
-
/>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ============================================
|
|
343
|
-
// SELECTABLE LIST COMPONENT
|
|
344
|
-
// ============================================
|
|
345
|
-
|
|
346
|
-
export interface SelectableItem {
|
|
347
|
-
id: string;
|
|
348
|
-
label: string;
|
|
349
|
-
description?: string;
|
|
350
|
-
icon?: string;
|
|
351
|
-
metadata?: Record<string, unknown>;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export interface SelectableListProps {
|
|
355
|
-
items: SelectableItem[];
|
|
356
|
-
selectedIndex?: number;
|
|
357
|
-
onSelect?: (item: SelectableItem, index: number) => void;
|
|
358
|
-
onActivate?: (item: SelectableItem, index: number) => void;
|
|
359
|
-
showIndices?: boolean;
|
|
360
|
-
showDescriptions?: boolean;
|
|
361
|
-
maxHeight?: number;
|
|
362
|
-
emptyMessage?: string;
|
|
363
|
-
/** Whether this list is focused (receives input) */
|
|
364
|
-
isFocused?: boolean;
|
|
365
|
-
/** Unique ID for input handling */
|
|
366
|
-
inputId?: string;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function SelectableList({
|
|
370
|
-
items,
|
|
371
|
-
selectedIndex = 0,
|
|
372
|
-
onSelect,
|
|
373
|
-
onActivate,
|
|
374
|
-
showIndices = true,
|
|
375
|
-
showDescriptions = true,
|
|
376
|
-
maxHeight = 10,
|
|
377
|
-
emptyMessage = "No items",
|
|
378
|
-
isFocused = true,
|
|
379
|
-
inputId = "selectable-list",
|
|
380
|
-
}: SelectableListProps) {
|
|
381
|
-
const [internalIndex, setInternalIndex] = useState(selectedIndex);
|
|
382
|
-
|
|
383
|
-
useEffect(() => {
|
|
384
|
-
setInternalIndex(selectedIndex);
|
|
385
|
-
}, [selectedIndex]);
|
|
386
|
-
|
|
387
|
-
// Handle keyboard input via centralized InputContext
|
|
388
|
-
const handleKey = useCallback((event: NativeKeyEvent): boolean => {
|
|
389
|
-
if (!isFocused || items.length === 0) return false;
|
|
390
|
-
|
|
391
|
-
// Up arrow or k
|
|
392
|
-
if ((event.code === "up" && event.kind === "press") ||
|
|
393
|
-
(event.code === "k" && !event.ctrl && event.kind === "press")) {
|
|
394
|
-
const newIndex = Math.max(0, internalIndex - 1);
|
|
395
|
-
setInternalIndex(newIndex);
|
|
396
|
-
const item = items[newIndex];
|
|
397
|
-
if (item) onSelect?.(item, newIndex);
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Down arrow or j
|
|
402
|
-
if ((event.code === "down" && event.kind === "press") ||
|
|
403
|
-
(event.code === "j" && !event.ctrl && event.kind === "press")) {
|
|
404
|
-
const newIndex = Math.min(items.length - 1, internalIndex + 1);
|
|
405
|
-
setInternalIndex(newIndex);
|
|
406
|
-
const item = items[newIndex];
|
|
407
|
-
if (item) onSelect?.(item, newIndex);
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Enter or l to activate
|
|
412
|
-
if ((event.code === "enter" && event.kind === "press") ||
|
|
413
|
-
(event.code === "l" && !event.ctrl && event.kind === "press")) {
|
|
414
|
-
const item = items[internalIndex];
|
|
415
|
-
if (item) {
|
|
416
|
-
onActivate?.(item, internalIndex);
|
|
417
|
-
}
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return false;
|
|
422
|
-
}, [isFocused, items, internalIndex, onSelect, onActivate]);
|
|
423
|
-
|
|
424
|
-
// Register with input system
|
|
425
|
-
useInputHandler(inputId, handleKey, {
|
|
426
|
-
priority: InputPriority.LIST,
|
|
427
|
-
isActive: isFocused && items.length > 0,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
if (items.length === 0) {
|
|
431
|
-
return (
|
|
432
|
-
<Box>
|
|
433
|
-
<Text dimColor>{emptyMessage}</Text>
|
|
434
|
-
</Box>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const visibleItems = items.slice(0, maxHeight);
|
|
439
|
-
|
|
440
|
-
return (
|
|
441
|
-
<Box flexDirection="column">
|
|
442
|
-
{visibleItems.map((item, index) => {
|
|
443
|
-
const isSelected = index === internalIndex;
|
|
444
|
-
const prefix = showIndices
|
|
445
|
-
? `${String(index + 1).padStart(2, " ")}. `
|
|
446
|
-
: "";
|
|
447
|
-
|
|
448
|
-
return (
|
|
449
|
-
<Box
|
|
450
|
-
key={item.id}
|
|
451
|
-
flexDirection="column"
|
|
452
|
-
marginBottom={1}
|
|
453
|
-
>
|
|
454
|
-
<Box>
|
|
455
|
-
{isSelected && (
|
|
456
|
-
<Text color="cyan" bold>{"->"} </Text>
|
|
457
|
-
)}
|
|
458
|
-
{!isSelected && <Text> </Text>}
|
|
459
|
-
<Text dimColor>{prefix}</Text>
|
|
460
|
-
{item.icon && <Text> {item.icon} </Text>}
|
|
461
|
-
<Text
|
|
462
|
-
color={isSelected ? "cyan" : "white"}
|
|
463
|
-
bold={isSelected}
|
|
464
|
-
>
|
|
465
|
-
{item.label}
|
|
466
|
-
</Text>
|
|
467
|
-
</Box>
|
|
468
|
-
{showDescriptions && item.description && (
|
|
469
|
-
<Box marginLeft={4}>
|
|
470
|
-
<Text dimColor>{item.description}</Text>
|
|
471
|
-
</Box>
|
|
472
|
-
)}
|
|
473
|
-
</Box>
|
|
474
|
-
);
|
|
475
|
-
})}
|
|
476
|
-
|
|
477
|
-
{items.length > maxHeight && (
|
|
478
|
-
<Box>
|
|
479
|
-
<Text dimColor>
|
|
480
|
-
... and {items.length - maxHeight} more (j to scroll)
|
|
481
|
-
</Text>
|
|
482
|
-
</Box>
|
|
483
|
-
)}
|
|
484
|
-
|
|
485
|
-
<Box marginTop={1}>
|
|
486
|
-
<Text dimColor>
|
|
487
|
-
up/down or j/k to navigate | Enter or l to select
|
|
488
|
-
</Text>
|
|
489
|
-
</Box>
|
|
490
|
-
</Box>
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// ============================================
|
|
495
|
-
// LINK COMPONENT (OSC 8 HYPERLINKS)
|
|
496
|
-
// ============================================
|
|
497
|
-
|
|
498
|
-
export interface LinkProps {
|
|
499
|
-
url: string;
|
|
500
|
-
text?: string;
|
|
501
|
-
fallback?: string; // Text to show if OSC 8 not supported
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Create an OSC 8 hyperlink
|
|
506
|
-
* OSC 8 format: ESC ] 8 ;; <url> BEL <text> ESC ] 8 ;; BEL
|
|
507
|
-
*/
|
|
508
|
-
export function createOsc8Link(url: string, text: string): string {
|
|
509
|
-
// OSC 8 hyperlink format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
|
|
510
|
-
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function Link({ url, text, fallback }: LinkProps) {
|
|
514
|
-
const displayText = text || url;
|
|
515
|
-
const fallbackText = fallback || displayText;
|
|
516
|
-
|
|
517
|
-
// Check if terminal likely supports OSC 8
|
|
518
|
-
// Most modern terminals do, but we provide fallback
|
|
519
|
-
const supportsOsc8 = process.env.TERM_PROGRAM === "iTerm.app" ||
|
|
520
|
-
process.env.TERM_PROGRAM === "Terminal.app" ||
|
|
521
|
-
process.env.TERM?.includes("xterm") ||
|
|
522
|
-
process.env.WT_SESSION_ID !== undefined; // Windows Terminal
|
|
523
|
-
|
|
524
|
-
if (supportsOsc8) {
|
|
525
|
-
// Render with OSC 8 hyperlink
|
|
526
|
-
// Note: Ink may escape this, so we use the raw text
|
|
527
|
-
return (
|
|
528
|
-
<Text color="blue">
|
|
529
|
-
{createOsc8Link(url, displayText)}
|
|
530
|
-
</Text>
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Fallback for terminals without OSC 8 support
|
|
535
|
-
return (
|
|
536
|
-
<Text color="blue" underline>
|
|
537
|
-
{fallbackText}
|
|
538
|
-
</Text>
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// ============================================
|
|
543
|
-
// CONTEXT PROVIDERS
|
|
544
|
-
// ============================================
|
|
545
|
-
|
|
546
|
-
interface InteractiveContextValue {
|
|
547
|
-
toast: ToastManager;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const InteractiveContext = createContext<InteractiveContextValue | null>(null);
|
|
551
|
-
|
|
552
|
-
export function InteractiveProvider({ children }: { children: React.ReactNode }) {
|
|
553
|
-
const toast = useToast();
|
|
554
|
-
|
|
555
|
-
return (
|
|
556
|
-
<InteractiveContext.Provider value={{ toast }}>
|
|
557
|
-
{children}
|
|
558
|
-
</InteractiveContext.Provider>
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
export function useInteractive(): InteractiveContextValue {
|
|
563
|
-
const context = useContext(InteractiveContext);
|
|
564
|
-
if (!context) {
|
|
565
|
-
throw new Error("useInteractive must be used within InteractiveProvider");
|
|
566
|
-
}
|
|
567
|
-
return context;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// ============================================
|
|
571
|
-
// EXPORTS
|
|
572
|
-
// ============================================
|
|
573
|
-
|
|
574
|
-
export default {
|
|
575
|
-
Toast,
|
|
576
|
-
useToast,
|
|
577
|
-
Modal,
|
|
578
|
-
ConfirmModal,
|
|
579
|
-
SelectableList,
|
|
580
|
-
Link,
|
|
581
|
-
createOsc8Link,
|
|
582
|
-
InteractiveProvider,
|
|
583
|
-
useInteractive,
|
|
584
|
-
};
|