aegiscode 3.1.7 → 3.1.10
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/README.md +23 -15
- package/dist/main.js +555 -556
- package/package.json +4 -2
- package/src/agent/Agent.ts +903 -0
- package/src/agent/SimpleAgent.ts +48 -0
- package/src/agent/index.ts +54 -0
- package/src/agent/orchestrator/AppBuilder.ts +443 -0
- package/src/agent/orchestrator/CouncilAgent.ts +310 -0
- package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
- package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
- package/src/agent/orchestrator/index.ts +38 -0
- package/src/agent/orchestrator/utils.ts +397 -0
- package/src/agent/pricing.ts +115 -0
- package/src/agent/router.ts +74 -0
- package/src/agent/routerStats.ts +121 -0
- package/src/agent/types.ts +318 -0
- package/src/auth/login.ts +383 -0
- package/src/cli/config.ts +189 -0
- package/src/cli/index.ts +17 -0
- package/src/cli/middleware.ts +119 -0
- package/src/cli/types.ts +75 -0
- package/src/config/ConfigManager.ts +587 -0
- package/src/config/index.ts +7 -0
- package/src/config/types.ts +584 -0
- package/src/context/CompactionService.ts +300 -0
- package/src/context/ContextManager.ts +450 -0
- package/src/context/FileAnalyzer.ts +267 -0
- package/src/context/TokenCounter.ts +265 -0
- package/src/context/index.ts +27 -0
- package/src/context/storage/CacheStore.ts +176 -0
- package/src/context/storage/JSONLStore.ts +201 -0
- package/src/context/storage/MemoryStore.ts +205 -0
- package/src/context/storage/PersistentStore.ts +327 -0
- package/src/context/storage/index.ts +9 -0
- package/src/context/storage/pathUtils.ts +114 -0
- package/src/context/test.ts +309 -0
- package/src/context/types.ts +268 -0
- package/src/hooks/HookExecutor.ts +434 -0
- package/src/hooks/HookManager.ts +596 -0
- package/src/hooks/HookService.ts +269 -0
- package/src/hooks/Matcher.ts +157 -0
- package/src/hooks/index.ts +63 -0
- package/src/hooks/types.ts +424 -0
- package/src/main.tsx +596 -0
- package/src/mcp/HealthMonitor.ts +150 -0
- package/src/mcp/McpClient.ts +491 -0
- package/src/mcp/McpRegistry.ts +321 -0
- package/src/mcp/createMcpTool.ts +251 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/server.ts +334 -0
- package/src/mcp/test-server.ts +88 -0
- package/src/mcp/test.ts +372 -0
- package/src/mcp/types.ts +247 -0
- package/src/memory/AgentMemoryBus.ts +432 -0
- package/src/memory/CloudSync.ts +99 -0
- package/src/memory/DriveSync.ts +106 -0
- package/src/memory/SharedMemory.ts +951 -0
- package/src/memory/index.ts +14 -0
- package/src/memory/machineFingerprint.ts +40 -0
- package/src/orchestrator/SubAgentMetadata.ts +136 -0
- package/src/prompts/builder.ts +213 -0
- package/src/prompts/default.ts +144 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/plan.ts +64 -0
- package/src/prompts/test.ts +78 -0
- package/src/services/AnthropicChatService.ts +341 -0
- package/src/services/ChatService.ts +347 -0
- package/src/services/ClaudeCliChatService.ts +256 -0
- package/src/services/CloudSync.ts +168 -0
- package/src/services/CostLedger.ts +211 -0
- package/src/services/Heartbeat.ts +135 -0
- package/src/services/LearningCollector.ts +291 -0
- package/src/services/OllamaInstaller.ts +342 -0
- package/src/services/VersionChecker.ts +445 -0
- package/src/services/index.ts +57 -0
- package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
- package/src/services/streaming/RenderingProfile.ts +90 -0
- package/src/services/streaming/StreamEventParser.ts +181 -0
- package/src/services/streaming/ThrottledRenderer.ts +139 -0
- package/src/services/streaming/TranscriptBuffer.ts +574 -0
- package/src/services/streaming/eventStatusMap.ts +52 -0
- package/src/services/streaming/index.ts +46 -0
- package/src/services/streaming/renderFormatting.ts +79 -0
- package/src/services/streaming/types.ts +234 -0
- package/src/skills/SkillLoader.ts +126 -0
- package/src/skills/SkillRegistry.ts +366 -0
- package/src/skills/index.ts +48 -0
- package/src/skills/types.ts +146 -0
- package/src/slash-commands/billing.ts +70 -0
- package/src/slash-commands/build.ts +413 -0
- package/src/slash-commands/builtinCommands.ts +2733 -0
- package/src/slash-commands/clone.ts +242 -0
- package/src/slash-commands/council.ts +125 -0
- package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
- package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
- package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
- package/src/slash-commands/custom/index.ts +7 -0
- package/src/slash-commands/debate.ts +254 -0
- package/src/slash-commands/gmail.ts +105 -0
- package/src/slash-commands/index.ts +388 -0
- package/src/slash-commands/mcpCommand.ts +205 -0
- package/src/slash-commands/types.ts +201 -0
- package/src/store/index.ts +76 -0
- package/src/store/selectors.ts +246 -0
- package/src/store/slices/appSlice.ts +205 -0
- package/src/store/slices/commandSlice.ts +115 -0
- package/src/store/slices/configSlice.ts +46 -0
- package/src/store/slices/focusSlice.ts +64 -0
- package/src/store/slices/index.ts +9 -0
- package/src/store/slices/sessionSlice.ts +424 -0
- package/src/store/streaming-buffer.ts +425 -0
- package/src/store/test.ts +296 -0
- package/src/store/types.ts +274 -0
- package/src/store/vanilla.ts +186 -0
- package/src/tools/builtin/bash.ts +236 -0
- package/src/tools/builtin/council.ts +105 -0
- package/src/tools/builtin/edit.ts +213 -0
- package/src/tools/builtin/glob.ts +136 -0
- package/src/tools/builtin/grep.ts +263 -0
- package/src/tools/builtin/index.ts +61 -0
- package/src/tools/builtin/memory.ts +66 -0
- package/src/tools/builtin/read.ts +168 -0
- package/src/tools/builtin/skill.ts +97 -0
- package/src/tools/builtin/snapshot.ts +40 -0
- package/src/tools/builtin/task.ts +106 -0
- package/src/tools/builtin/write.ts +134 -0
- package/src/tools/createTool.ts +221 -0
- package/src/tools/execution/ExecutionPipeline.ts +263 -0
- package/src/tools/execution/index.ts +40 -0
- package/src/tools/execution/stages/CacheStage.ts +131 -0
- package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
- package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
- package/src/tools/execution/stages/ExecutionStage.ts +48 -0
- package/src/tools/execution/stages/FormattingStage.ts +44 -0
- package/src/tools/execution/stages/HookStage.ts +72 -0
- package/src/tools/execution/stages/PermissionStage.ts +287 -0
- package/src/tools/execution/stages/PostHookStage.ts +71 -0
- package/src/tools/execution/stages/index.ts +12 -0
- package/src/tools/execution/test.ts +266 -0
- package/src/tools/execution/types.ts +273 -0
- package/src/tools/index.ts +81 -0
- package/src/tools/registry.ts +304 -0
- package/src/tools/schemas.ts +109 -0
- package/src/tools/test.ts +220 -0
- package/src/tools/types.ts +175 -0
- package/src/tools/validation/PermissionChecker.ts +242 -0
- package/src/tools/validation/SensitiveFileDetector.ts +210 -0
- package/src/tools/validation/index.ts +11 -0
- package/src/ui/App.tsx +166 -0
- package/src/ui/components/AegisInterface.tsx +484 -0
- package/src/ui/components/common/ChatSearch.tsx +150 -0
- package/src/ui/components/common/ErrorBoundary.tsx +82 -0
- package/src/ui/components/common/ExitMessage.tsx +120 -0
- package/src/ui/components/common/LoadingIndicator.tsx +49 -0
- package/src/ui/components/common/index.ts +6 -0
- package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
- package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
- package/src/ui/components/dialog/SetupWizard.tsx +297 -0
- package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
- package/src/ui/components/dialog/index.ts +8 -0
- package/src/ui/components/index.ts +28 -0
- package/src/ui/components/input/CommandSuggestions.tsx +139 -0
- package/src/ui/components/input/CustomTextInput.tsx +220 -0
- package/src/ui/components/input/InputArea.tsx +361 -0
- package/src/ui/components/input/PromptSuggestions.tsx +66 -0
- package/src/ui/components/input/index.ts +6 -0
- package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
- package/src/ui/components/layout/ContextBar.tsx +79 -0
- package/src/ui/components/layout/MessageArea.tsx +96 -0
- package/src/ui/components/layout/MessageList.tsx +647 -0
- package/src/ui/components/layout/MessageSeparator.tsx +26 -0
- package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
- package/src/ui/components/layout/index.ts +7 -0
- package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
- package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
- package/src/ui/components/markdown/index.ts +8 -0
- package/src/ui/components/markdown/parser.ts +336 -0
- package/src/ui/components/markdown/types.ts +66 -0
- package/src/ui/focus/FocusManager.ts +137 -0
- package/src/ui/focus/index.ts +13 -0
- package/src/ui/focus/types.ts +54 -0
- package/src/ui/focus/useFocus.ts +75 -0
- package/src/ui/hooks/index.ts +11 -0
- package/src/ui/hooks/useAgent.ts +284 -0
- package/src/ui/hooks/useCommandHistory.ts +87 -0
- package/src/ui/hooks/useCommandProcessor.ts +443 -0
- package/src/ui/hooks/useConfirmation.ts +99 -0
- package/src/ui/hooks/useCtrlCHandler.ts +100 -0
- package/src/ui/hooks/useInputBuffer.ts +122 -0
- package/src/ui/hooks/useTerminalSize.ts +68 -0
- package/src/ui/hooks/useTerminalWidth.ts +5 -0
- package/src/ui/hooks/useWindowedList.ts +118 -0
- package/src/ui/render-debugger.ts +621 -0
- package/src/ui/test.ts +189 -0
- package/src/ui/themes/ThemeManager.ts +332 -0
- package/src/ui/themes/aegisTheme.ts +87 -0
- package/src/ui/themes/darkTheme.ts +87 -0
- package/src/ui/themes/defaultTheme.ts +85 -0
- package/src/ui/themes/index.ts +10 -0
- package/src/ui/themes/lightTheme.ts +89 -0
- package/src/ui/themes/popularThemes.ts +187 -0
- package/src/ui/themes/types.ts +130 -0
- package/src/utils/clipboard.ts +48 -0
- package/src/utils/debug.ts +43 -0
- package/src/utils/environment.ts +68 -0
- package/src/utils/index.ts +10 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInputBuffer - 输入缓冲区管理
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
8
|
+
|
|
9
|
+
interface InputBufferResult {
|
|
10
|
+
/** 当前输入值 */
|
|
11
|
+
value: string;
|
|
12
|
+
/** 光标位置 */
|
|
13
|
+
cursorPosition: number;
|
|
14
|
+
/** 设置输入值 */
|
|
15
|
+
setValue: (newValue: string) => void;
|
|
16
|
+
/** 设置光标位置 */
|
|
17
|
+
setCursorPosition: (pos: number) => void;
|
|
18
|
+
/** 在光标位置插入文本 */
|
|
19
|
+
insertAt: (text: string) => void;
|
|
20
|
+
/** 删除光标前的字符 */
|
|
21
|
+
deleteBackward: () => void;
|
|
22
|
+
/** 删除光标后的字符 */
|
|
23
|
+
deleteForward: () => void;
|
|
24
|
+
/** 清空输入 */
|
|
25
|
+
clear: () => void;
|
|
26
|
+
/** 移动光标到开头 */
|
|
27
|
+
moveToStart: () => void;
|
|
28
|
+
/** 移动光标到结尾 */
|
|
29
|
+
moveToEnd: () => void;
|
|
30
|
+
/** 获取当前引用(用于避免重渲染时的闭包问题) */
|
|
31
|
+
getRef: () => { value: string; cursorPosition: number };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
export const useInputBuffer = (
|
|
38
|
+
initialValue = '',
|
|
39
|
+
initialCursor = 0
|
|
40
|
+
): InputBufferResult => {
|
|
41
|
+
const [value, setValueState] = useState(initialValue);
|
|
42
|
+
const [cursorPosition, setCursorPositionState] = useState(initialCursor);
|
|
43
|
+
|
|
44
|
+
// 稳定的引用,避免 resize 时重
|
|
45
|
+
const bufferRef = useRef({ value, cursorPosition });
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
bufferRef.current = { value, cursorPosition };
|
|
49
|
+
}, [value, cursorPosition]);
|
|
50
|
+
|
|
51
|
+
const setValue = useCallback((newValue: string) => {
|
|
52
|
+
// 立即更新 ref,避免 setCursorPosition 使用旧的长度限
|
|
53
|
+
bufferRef.current.value = newValue;
|
|
54
|
+
setValueState(newValue);
|
|
55
|
+
// 确保光标不超出范
|
|
56
|
+
setCursorPositionState(prev => Math.min(prev, newValue.length));
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const setCursorPosition = useCallback((pos: number) => {
|
|
60
|
+
const newPos = Math.max(0, Math.min(pos, bufferRef.current.value.length));
|
|
61
|
+
// 立即更新 ref,保持同
|
|
62
|
+
bufferRef.current.cursorPosition = newPos;
|
|
63
|
+
setCursorPositionState(newPos);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const insertAt = useCallback((text: string) => {
|
|
67
|
+
setValueState(prev => {
|
|
68
|
+
const pos = bufferRef.current.cursorPosition;
|
|
69
|
+
const newValue = prev.slice(0, pos) + text + prev.slice(pos);
|
|
70
|
+
return newValue;
|
|
71
|
+
});
|
|
72
|
+
setCursorPositionState(prev => prev + text.length);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const deleteBackward = useCallback(() => {
|
|
76
|
+
if (bufferRef.current.cursorPosition > 0) {
|
|
77
|
+
setValueState(prev => {
|
|
78
|
+
const pos = bufferRef.current.cursorPosition;
|
|
79
|
+
return prev.slice(0, pos - 1) + prev.slice(pos);
|
|
80
|
+
});
|
|
81
|
+
setCursorPositionState(prev => prev - 1);
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const deleteForward = useCallback(() => {
|
|
86
|
+
if (bufferRef.current.cursorPosition < bufferRef.current.value.length) {
|
|
87
|
+
setValueState(prev => {
|
|
88
|
+
const pos = bufferRef.current.cursorPosition;
|
|
89
|
+
return prev.slice(0, pos) + prev.slice(pos + 1);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const clear = useCallback(() => {
|
|
95
|
+
setValueState('');
|
|
96
|
+
setCursorPositionState(0);
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const moveToStart = useCallback(() => {
|
|
100
|
+
setCursorPositionState(0);
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const moveToEnd = useCallback(() => {
|
|
104
|
+
setCursorPositionState(bufferRef.current.value.length);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const getRef = useCallback(() => bufferRef.current, []);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
value,
|
|
111
|
+
cursorPosition,
|
|
112
|
+
setValue,
|
|
113
|
+
setCursorPosition,
|
|
114
|
+
insertAt,
|
|
115
|
+
deleteBackward,
|
|
116
|
+
deleteForward,
|
|
117
|
+
clear,
|
|
118
|
+
moveToStart,
|
|
119
|
+
moveToEnd,
|
|
120
|
+
getRef,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTerminalSize - Combined terminal size hook with RAF debounce + resize observer
|
|
3
|
+
*
|
|
4
|
+
* Consolidates useTerminalWidth and useTerminalHeight into a single hook
|
|
5
|
+
* to avoid duplicate RAF loops and ensure width/height are always in sync.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useRef } from 'react';
|
|
9
|
+
|
|
10
|
+
interface TerminalSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSize(): TerminalSize {
|
|
16
|
+
return {
|
|
17
|
+
width: process.stdout.columns || 80,
|
|
18
|
+
height: process.stdout.rows || 24,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEBOUNCE_MS = 80; // Wait 80ms after last resize event before updating
|
|
23
|
+
|
|
24
|
+
export function useTerminalSize(): TerminalSize {
|
|
25
|
+
const [size, setSize] = useState(getSize);
|
|
26
|
+
const rafRef = useRef<number | null>(null);
|
|
27
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
28
|
+
const sizeRef = useRef(size);
|
|
29
|
+
sizeRef.current = size;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const handleResize = () => {
|
|
33
|
+
// RAF-throttle: skip if a frame is already queued
|
|
34
|
+
if (rafRef.current !== null) return;
|
|
35
|
+
|
|
36
|
+
// Debounce: restart timer on each resize event
|
|
37
|
+
if (debounceTimerRef.current !== null) {
|
|
38
|
+
clearTimeout(debounceTimerRef.current);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
42
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
43
|
+
const newSize = getSize();
|
|
44
|
+
const prev = sizeRef.current;
|
|
45
|
+
// Only update if either dimension changed
|
|
46
|
+
if (newSize.width !== prev.width || newSize.height !== prev.height) {
|
|
47
|
+
setSize(newSize);
|
|
48
|
+
}
|
|
49
|
+
rafRef.current = null;
|
|
50
|
+
});
|
|
51
|
+
debounceTimerRef.current = null;
|
|
52
|
+
}, DEBOUNCE_MS);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
process.stdout.on('resize', handleResize);
|
|
56
|
+
return () => {
|
|
57
|
+
process.stdout.off('resize', handleResize);
|
|
58
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
59
|
+
if (debounceTimerRef.current !== null) clearTimeout(debounceTimerRef.current);
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return size;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Re-export individual hooks for backward compatibility
|
|
67
|
+
export const useTerminalWidth = (): number => useTerminalSize().width;
|
|
68
|
+
export const useTerminalHeight = (): number => useTerminalSize().height;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWindowedList - Virtual list rendering hook for Ink
|
|
3
|
+
*
|
|
4
|
+
* Only renders items within the visible window, drastically reducing
|
|
5
|
+
* render cost for long message lists.
|
|
6
|
+
*
|
|
7
|
+
* Uses a fixed item height estimate for simplicity. Ink doesn't give us
|
|
8
|
+
* actual item heights, so we estimate based on content length.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
|
12
|
+
|
|
13
|
+
interface WindowedListResult<T> {
|
|
14
|
+
/** The subset of items to render */
|
|
15
|
+
visibleItems: Array<{ item: T; index: number }>;
|
|
16
|
+
/** Total number of items */
|
|
17
|
+
totalItems: number;
|
|
18
|
+
/** Render padding at top to maintain scroll position */
|
|
19
|
+
topPadding: number;
|
|
20
|
+
/** Render padding at bottom */
|
|
21
|
+
bottomPadding: number;
|
|
22
|
+
/** Call when user scrolls up/down */
|
|
23
|
+
scrollTo: (index: number) => void;
|
|
24
|
+
/** Scroll to bottom (latest messages) */
|
|
25
|
+
scrollToBottom: () => void;
|
|
26
|
+
/** Whether we're pinned to bottom */
|
|
27
|
+
isAtBottom: boolean;
|
|
28
|
+
/** Ref to attach to scroll container */
|
|
29
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ESTIMATED_ITEM_HEIGHT = 3; // rows per message average
|
|
33
|
+
const OVERSCAN = 3; // extra items above/below viewport
|
|
34
|
+
|
|
35
|
+
function calcVisibleCount(terminalHeight: number): number {
|
|
36
|
+
const visibleHeight = Math.max(terminalHeight - 5, 5);
|
|
37
|
+
return Math.ceil(visibleHeight / ESTIMATED_ITEM_HEIGHT) + OVERSCAN * 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useWindowedList<T>(
|
|
41
|
+
items: T[],
|
|
42
|
+
terminalHeight: number,
|
|
43
|
+
): WindowedListResult<T> {
|
|
44
|
+
const visibleCount = calcVisibleCount(terminalHeight);
|
|
45
|
+
|
|
46
|
+
const [scrollIndex, setScrollIndex] = useState<number>(() =>
|
|
47
|
+
Math.max(0, items.length - visibleCount),
|
|
48
|
+
);
|
|
49
|
+
const prevLenRef = useRef(items.length);
|
|
50
|
+
const isAtBottomRef = useRef(true);
|
|
51
|
+
|
|
52
|
+
// Track latest items length via ref to avoid callback recreations
|
|
53
|
+
const itemsLenRef = useRef(items.length);
|
|
54
|
+
itemsLenRef.current = items.length;
|
|
55
|
+
|
|
56
|
+
// Auto-scroll to bottom when new items arrive (if already at bottom)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const prevLen = prevLenRef.current;
|
|
59
|
+
const currentLen = items.length;
|
|
60
|
+
if (currentLen > prevLen && isAtBottomRef.current) {
|
|
61
|
+
setScrollIndex(Math.max(0, currentLen - visibleCount));
|
|
62
|
+
}
|
|
63
|
+
prevLenRef.current = currentLen;
|
|
64
|
+
}, [items.length, visibleCount]);
|
|
65
|
+
|
|
66
|
+
const [committedScrollIndex, setCommittedScrollIndex] = useState(scrollIndex);
|
|
67
|
+
|
|
68
|
+
const isAtBottom = useMemo(
|
|
69
|
+
() => committedScrollIndex + visibleCount >= items.length,
|
|
70
|
+
[committedScrollIndex, visibleCount, items.length],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const scrollTo = useCallback((index: number) => {
|
|
74
|
+
const len = itemsLenRef.current;
|
|
75
|
+
const vCount = calcVisibleCount(terminalHeight);
|
|
76
|
+
const clamped = Math.max(0, Math.min(index, len - 1));
|
|
77
|
+
setScrollIndex(clamped);
|
|
78
|
+
setCommittedScrollIndex(clamped);
|
|
79
|
+
isAtBottomRef.current = clamped + vCount >= len;
|
|
80
|
+
}, [terminalHeight]);
|
|
81
|
+
|
|
82
|
+
const scrollToBottom = useCallback(() => {
|
|
83
|
+
const len = itemsLenRef.current;
|
|
84
|
+
const vCount = calcVisibleCount(terminalHeight);
|
|
85
|
+
const bottom = Math.max(0, len - vCount);
|
|
86
|
+
setScrollIndex(bottom);
|
|
87
|
+
setCommittedScrollIndex(bottom);
|
|
88
|
+
isAtBottomRef.current = true;
|
|
89
|
+
}, [terminalHeight]);
|
|
90
|
+
|
|
91
|
+
// Compute visible window
|
|
92
|
+
const startIndex = Math.max(0, committedScrollIndex - OVERSCAN);
|
|
93
|
+
const endIndex = Math.min(items.length, committedScrollIndex + visibleCount + OVERSCAN);
|
|
94
|
+
|
|
95
|
+
const visibleItems = useMemo(
|
|
96
|
+
() => items.slice(startIndex, endIndex).map((item, i) => ({
|
|
97
|
+
item,
|
|
98
|
+
index: startIndex + i,
|
|
99
|
+
})),
|
|
100
|
+
[items, startIndex, endIndex],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const topPadding = startIndex * ESTIMATED_ITEM_HEIGHT;
|
|
104
|
+
const bottomPadding = (items.length - endIndex) * ESTIMATED_ITEM_HEIGHT;
|
|
105
|
+
|
|
106
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
visibleItems,
|
|
110
|
+
totalItems: items.length,
|
|
111
|
+
topPadding,
|
|
112
|
+
bottomPadding,
|
|
113
|
+
scrollTo,
|
|
114
|
+
scrollToBottom,
|
|
115
|
+
isAtBottom,
|
|
116
|
+
containerRef,
|
|
117
|
+
} as any; // Type cast for Ink compatibility
|
|
118
|
+
}
|