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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Buffer - external mutable buffer for streaming content.
|
|
3
|
+
*
|
|
4
|
+
* This completely bypasses the zustand store during streaming deltas.
|
|
5
|
+
* Instead of calling set() on every flush (which triggers React re-renders
|
|
6
|
+
* across all store subscribers), we write directly to a mutable buffer.
|
|
7
|
+
* The MessageList RAF loop reads from this buffer directly.
|
|
8
|
+
*
|
|
9
|
+
* Only start/finish streaming and explicit flushes update the store.
|
|
10
|
+
*
|
|
11
|
+
* Supports Content Block model (Claude-style): text, thinking, tool_use, tool_result.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SessionMessage, ToolCallStatus, ContentBlock } from './types.js';
|
|
15
|
+
import { TranscriptBuffer } from '../services/streaming/TranscriptBuffer.js';
|
|
16
|
+
import { parseStreamEvent } from '../services/streaming/StreamEventParser.js';
|
|
17
|
+
import type { AnthropicStreamEvent } from '../services/streaming/types.js';
|
|
18
|
+
|
|
19
|
+
// Shared mutable state for the currently streaming message
|
|
20
|
+
interface StreamingState {
|
|
21
|
+
messageId: string | null;
|
|
22
|
+
content: string;
|
|
23
|
+
thinking: string;
|
|
24
|
+
// Content block tracking
|
|
25
|
+
currentBlockType: 'text' | 'thinking' | null;
|
|
26
|
+
currentBlockAccumulator: string;
|
|
27
|
+
toolCalls: Map<string, { name: string; arguments: string; status: ToolCallStatus }>;
|
|
28
|
+
/** Canonical rich segment state — single source of truth for streaming content */
|
|
29
|
+
transcriptBuffer: TranscriptBuffer;
|
|
30
|
+
/** Maps content block index → tool use ID for input_json_delta accumulation */
|
|
31
|
+
blockIndexToToolId: Map<number, string>;
|
|
32
|
+
/** Timestamp of last content write — used to measure render latency */
|
|
33
|
+
lastWriteTimestamp: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const streamingState: StreamingState = {
|
|
37
|
+
messageId: null,
|
|
38
|
+
content: '',
|
|
39
|
+
thinking: '',
|
|
40
|
+
currentBlockType: null,
|
|
41
|
+
currentBlockAccumulator: '',
|
|
42
|
+
toolCalls: new Map(),
|
|
43
|
+
transcriptBuffer: new TranscriptBuffer({ showToolResults: false }),
|
|
44
|
+
blockIndexToToolId: new Map(),
|
|
45
|
+
lastWriteTimestamp: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a message IS the currently active streaming message (by ref).
|
|
50
|
+
* This is used by the RAF loop to efficiently skip non-streaming messages.
|
|
51
|
+
*/
|
|
52
|
+
export function isActiveStreamingMessage(msg: SessionMessage): boolean {
|
|
53
|
+
return msg.id === streamingState.messageId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get direct reference to current streaming content.
|
|
58
|
+
* Returns null if no streaming is active.
|
|
59
|
+
*/
|
|
60
|
+
export function getStreamingContent(): { content: string; thinking: string } | null {
|
|
61
|
+
if (!streamingState.messageId) return null;
|
|
62
|
+
return {
|
|
63
|
+
content: streamingState.content,
|
|
64
|
+
thinking: streamingState.thinking,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get streaming content blocks synthesized from the current buffer state.
|
|
70
|
+
* Returns TextBlock and ThinkingBlock objects representing in-flight content,
|
|
71
|
+
* plus any accumulated tool_use blocks.
|
|
72
|
+
*
|
|
73
|
+
* These are merged with store content blocks in MessageList for structured rendering.
|
|
74
|
+
*/
|
|
75
|
+
export function getStreamingContentBlocks(): ContentBlock[] {
|
|
76
|
+
const blocks: ContentBlock[] = [];
|
|
77
|
+
|
|
78
|
+
if (streamingState.thinking) {
|
|
79
|
+
blocks.push({ type: 'thinking', thinking: streamingState.thinking });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (streamingState.content) {
|
|
83
|
+
blocks.push({ type: 'text', text: streamingState.content });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return blocks;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply a raw Anthropic-format streaming event.
|
|
91
|
+
*
|
|
92
|
+
* This is the primary entry point for the main chat path. It feeds the
|
|
93
|
+
* TranscriptBuffer (rich segment model) and simultaneously updates the
|
|
94
|
+
* mutable content/thinking strings (for drain/flush compat) and the
|
|
95
|
+
* toolCalls map (for arg accumulation before store flush).
|
|
96
|
+
*
|
|
97
|
+
* Legacy paths (slash commands) continue using appendToBuffer /
|
|
98
|
+
* appendThinkingToBuffer directly and bypass the TranscriptBuffer.
|
|
99
|
+
*/
|
|
100
|
+
export function applyStreamEvent(event: AnthropicStreamEvent): void {
|
|
101
|
+
const parsed = parseStreamEvent(event);
|
|
102
|
+
for (const e of parsed) {
|
|
103
|
+
streamingState.transcriptBuffer.apply(e);
|
|
104
|
+
|
|
105
|
+
if ((e.type === 'text_delta' || e.type === 'text_chunk') && e.text) {
|
|
106
|
+
streamingState.content += e.text;
|
|
107
|
+
streamingState.lastWriteTimestamp = Date.now();
|
|
108
|
+
streamingState.currentBlockType = 'text';
|
|
109
|
+
streamingState.currentBlockAccumulator += e.text;
|
|
110
|
+
}
|
|
111
|
+
if ((e.type === 'thinking_delta' || e.type === 'thinking_chunk') && e.text) {
|
|
112
|
+
streamingState.thinking += e.text;
|
|
113
|
+
streamingState.lastWriteTimestamp = Date.now();
|
|
114
|
+
streamingState.currentBlockType = 'thinking';
|
|
115
|
+
streamingState.currentBlockAccumulator += e.text;
|
|
116
|
+
}
|
|
117
|
+
if (e.type === 'tool_use_start' && e.id && e.name) {
|
|
118
|
+
if (!streamingState.toolCalls.has(e.id)) {
|
|
119
|
+
streamingState.toolCalls.set(e.id, { name: e.name, arguments: '', status: 'running' });
|
|
120
|
+
}
|
|
121
|
+
if (e.index !== undefined && e.index >= 0) {
|
|
122
|
+
streamingState.blockIndexToToolId.set(e.index, e.id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (e.type === 'tool_use_delta' && e.partial_json && e.index !== undefined) {
|
|
126
|
+
const toolId = streamingState.blockIndexToToolId.get(e.index);
|
|
127
|
+
if (toolId) {
|
|
128
|
+
const tc = streamingState.toolCalls.get(toolId);
|
|
129
|
+
if (tc) tc.arguments += e.partial_json;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Initialize buffer for a new streaming message.
|
|
137
|
+
* When called with the same ID as the current message (flush cycle),
|
|
138
|
+
* only the content/thinking strings are reset — tool calls are preserved
|
|
139
|
+
* so arg accumulation survives mid-stream flushes.
|
|
140
|
+
*/
|
|
141
|
+
export function initStreamingBuffer(messageId: string): void {
|
|
142
|
+
const isNewMessage = streamingState.messageId !== messageId;
|
|
143
|
+
streamingState.messageId = messageId;
|
|
144
|
+
streamingState.content = '';
|
|
145
|
+
streamingState.thinking = '';
|
|
146
|
+
streamingState.currentBlockType = null;
|
|
147
|
+
streamingState.currentBlockAccumulator = '';
|
|
148
|
+
if (isNewMessage) {
|
|
149
|
+
streamingState.toolCalls.clear();
|
|
150
|
+
streamingState.transcriptBuffer.clear();
|
|
151
|
+
streamingState.blockIndexToToolId.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append content delta to the mutable buffer (NO store update).
|
|
157
|
+
* Tracks as 'text' content block if we're in a text block.
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* Get the time (ms) since the last content was written to the buffer.
|
|
161
|
+
* Returns 0 if no streaming is active.
|
|
162
|
+
*/
|
|
163
|
+
export function getStreamingLatencyMs(): number {
|
|
164
|
+
if (!streamingState.messageId || streamingState.lastWriteTimestamp === 0) return 0;
|
|
165
|
+
return Date.now() - streamingState.lastWriteTimestamp;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function appendToBuffer(contentDelta: string): void {
|
|
169
|
+
streamingState.content += contentDelta;
|
|
170
|
+
streamingState.lastWriteTimestamp = Date.now();
|
|
171
|
+
// Track as text block
|
|
172
|
+
streamingState.currentBlockType = 'text';
|
|
173
|
+
streamingState.currentBlockAccumulator += contentDelta;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Append thinking delta to the mutable buffer (NO store update).
|
|
178
|
+
* Tracks as 'thinking' content block.
|
|
179
|
+
*/
|
|
180
|
+
export function appendThinkingToBuffer(thinkingDelta: string): void {
|
|
181
|
+
streamingState.thinking += thinkingDelta;
|
|
182
|
+
streamingState.lastWriteTimestamp = Date.now();
|
|
183
|
+
streamingState.currentBlockType = 'thinking';
|
|
184
|
+
streamingState.currentBlockAccumulator += thinkingDelta;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Signal the start of a tool_use block. Records the tool call ID and name.
|
|
189
|
+
*/
|
|
190
|
+
export function startToolCallInBuffer(toolCallId: string, name: string): void {
|
|
191
|
+
streamingState.toolCalls.set(toolCallId, { name, arguments: '', status: 'running' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Accumulate tool call arguments JSON delta.
|
|
196
|
+
*/
|
|
197
|
+
export function appendToolCallDelta(toolCallId: string, argumentsDelta: string): void {
|
|
198
|
+
const existing = streamingState.toolCalls.get(toolCallId);
|
|
199
|
+
if (existing) {
|
|
200
|
+
existing.arguments += argumentsDelta;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Mark a tool call as completed (success or error).
|
|
206
|
+
*/
|
|
207
|
+
export function finishToolCallInBuffer(toolCallId: string, isError: boolean): void {
|
|
208
|
+
const existing = streamingState.toolCalls.get(toolCallId);
|
|
209
|
+
if (existing) {
|
|
210
|
+
existing.status = isError ? 'error' : 'success';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get tool calls accumulated in the buffer.
|
|
216
|
+
*/
|
|
217
|
+
export function getBufferedToolCalls(): Array<{ id: string; name: string; arguments: string; status: ToolCallStatus }> {
|
|
218
|
+
return Array.from(streamingState.toolCalls.entries()).map(([id, tc]) => ({
|
|
219
|
+
id,
|
|
220
|
+
...tc,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear the buffer (e.g., on error/abort or after finishStreamingMessage).
|
|
226
|
+
*/
|
|
227
|
+
export function clearBuffer(): void {
|
|
228
|
+
streamingState.messageId = null;
|
|
229
|
+
streamingState.content = '';
|
|
230
|
+
streamingState.thinking = '';
|
|
231
|
+
streamingState.currentBlockType = null;
|
|
232
|
+
streamingState.currentBlockAccumulator = '';
|
|
233
|
+
streamingState.toolCalls.clear();
|
|
234
|
+
streamingState.transcriptBuffer.clear();
|
|
235
|
+
streamingState.blockIndexToToolId.clear();
|
|
236
|
+
streamingState.lastWriteTimestamp = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if buffer has content.
|
|
241
|
+
*/
|
|
242
|
+
export function hasBufferContent(): boolean {
|
|
243
|
+
return streamingState.content.length > 0 || streamingState.thinking.length > 0 || streamingState.toolCalls.size > 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Peek at current content without clearing.
|
|
248
|
+
*/
|
|
249
|
+
export function peekBuffer(): { content: string; thinking: string; currentBlockType: string | null; currentBlockContent: string; toolCalls: Array<{ id: string; name: string; arguments: string; status: ToolCallStatus }> } {
|
|
250
|
+
return {
|
|
251
|
+
content: streamingState.content,
|
|
252
|
+
thinking: streamingState.thinking,
|
|
253
|
+
currentBlockType: streamingState.currentBlockType,
|
|
254
|
+
currentBlockContent: streamingState.currentBlockAccumulator,
|
|
255
|
+
toolCalls: getBufferedToolCalls(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Drain accumulated content block from the buffer.
|
|
261
|
+
* Returns the block data and resets the accumulator.
|
|
262
|
+
*/
|
|
263
|
+
export function drainContentBlock(): { type: 'text' | 'thinking'; content: string } | null {
|
|
264
|
+
if (!streamingState.currentBlockType || !streamingState.currentBlockAccumulator) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const block = {
|
|
268
|
+
type: streamingState.currentBlockType as 'text' | 'thinking',
|
|
269
|
+
content: streamingState.currentBlockAccumulator,
|
|
270
|
+
};
|
|
271
|
+
streamingState.currentBlockType = null;
|
|
272
|
+
streamingState.currentBlockAccumulator = '';
|
|
273
|
+
return block;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Drain tool calls from the buffer.
|
|
278
|
+
*/
|
|
279
|
+
export function drainToolCalls(): Array<{ id: string; name: string; arguments: string; status: ToolCallStatus }> {
|
|
280
|
+
const calls = getBufferedToolCalls();
|
|
281
|
+
streamingState.toolCalls.clear();
|
|
282
|
+
return calls;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Drain ALL content from the buffer and return it.
|
|
287
|
+
* Clears the buffer entirely. Used by flushStreamBuffer and finishStreamingMessage.
|
|
288
|
+
*
|
|
289
|
+
* This is the SINGLE function that removes content from the buffer.
|
|
290
|
+
* After calling it, the buffer is empty and ready for new content.
|
|
291
|
+
*
|
|
292
|
+
* Returns null if buffer is empty (no messageId set or no content).
|
|
293
|
+
*/
|
|
294
|
+
export function drainBuffer(): { content: string; thinking: string; toolCalls: Array<{ id: string; name: string; arguments: string; status: ToolCallStatus }> } | null {
|
|
295
|
+
if (!streamingState.messageId) return null;
|
|
296
|
+
if (!streamingState.content && !streamingState.thinking && streamingState.toolCalls.size === 0) return null;
|
|
297
|
+
|
|
298
|
+
const result = {
|
|
299
|
+
content: streamingState.content,
|
|
300
|
+
thinking: streamingState.thinking,
|
|
301
|
+
toolCalls: getBufferedToolCalls(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Clear content but KEEP messageId (so isActiveStreamingMessage still works)
|
|
305
|
+
// and KEEP toolCalls (they're still in-flight; cleared by clearBuffer after finish).
|
|
306
|
+
streamingState.content = '';
|
|
307
|
+
streamingState.thinking = '';
|
|
308
|
+
streamingState.currentBlockType = null;
|
|
309
|
+
streamingState.currentBlockAccumulator = '';
|
|
310
|
+
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ==================== Batch Buffer for Slash Commands ====================
|
|
315
|
+
// Slash commands trigger multiple store updates (addUserMessage, setThinking, addAssistantMessage).
|
|
316
|
+
// Instead of each calling set() individually (causing cascading re-renders), we batch them:
|
|
317
|
+
// 1. Collect all mutations in a pending batch object
|
|
318
|
+
// 2. flushBatch() applies them as a single set() call
|
|
319
|
+
//
|
|
320
|
+
// The MessageList subscription already detects message count changes (see MessageList.tsx:128-133),
|
|
321
|
+
// so a single batch flush triggers exactly ONE re-render for the entire slash command result.
|
|
322
|
+
|
|
323
|
+
interface BatchUpdate {
|
|
324
|
+
userMessage?: string;
|
|
325
|
+
assistantMessage?: string;
|
|
326
|
+
isThinking?: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let pendingBatch: BatchUpdate | null = null;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Start collecting mutations into a batch.
|
|
333
|
+
*/
|
|
334
|
+
export function startBatch(): void {
|
|
335
|
+
pendingBatch = {};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Queue a user message in the current batch.
|
|
340
|
+
*/
|
|
341
|
+
export function batchAddUserMessage(content: string): void {
|
|
342
|
+
if (pendingBatch) {
|
|
343
|
+
pendingBatch.userMessage = content;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Queue an assistant message in the current batch.
|
|
349
|
+
*/
|
|
350
|
+
export function batchAddAssistantMessage(content: string): void {
|
|
351
|
+
if (pendingBatch) {
|
|
352
|
+
pendingBatch.assistantMessage = content;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Queue a thinking state change in the current batch.
|
|
358
|
+
*/
|
|
359
|
+
export function batchSetThinking(isThinking: boolean): void {
|
|
360
|
+
if (pendingBatch) {
|
|
361
|
+
pendingBatch.isThinking = isThinking;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Flush the batch: apply all queued mutations as a single set() call.
|
|
367
|
+
* Accepts the store instance to avoid circular dependencies in ESM.
|
|
368
|
+
*
|
|
369
|
+
* Returns true if any mutations were applied.
|
|
370
|
+
*
|
|
371
|
+
* This is equivalent to calling:
|
|
372
|
+
* sessionActions().addUserMessage(...) + sessionActions().addAssistantMessage(...) + sessionActions().setThinking(...)
|
|
373
|
+
* but done as a SINGLE setState call, eliminating cascading re-renders.
|
|
374
|
+
*/
|
|
375
|
+
export function flushBatchWithStore(store: any): boolean {
|
|
376
|
+
if (!pendingBatch) return false;
|
|
377
|
+
const batch = pendingBatch;
|
|
378
|
+
pendingBatch = null;
|
|
379
|
+
|
|
380
|
+
const hasUser = batch.userMessage !== undefined;
|
|
381
|
+
const hasAssistant = batch.assistantMessage !== undefined;
|
|
382
|
+
const hasThinking = batch.isThinking !== undefined;
|
|
383
|
+
|
|
384
|
+
if (!hasUser && !hasAssistant && !hasThinking) return false;
|
|
385
|
+
|
|
386
|
+
const current = store.getState().session;
|
|
387
|
+
const newMessages = [...current.messages];
|
|
388
|
+
const generateId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
389
|
+
|
|
390
|
+
if (hasUser && batch.userMessage) {
|
|
391
|
+
newMessages.push({
|
|
392
|
+
id: `user-${generateId()}`,
|
|
393
|
+
role: 'user' as const,
|
|
394
|
+
content: batch.userMessage,
|
|
395
|
+
timestamp: Date.now(),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (hasAssistant && batch.assistantMessage) {
|
|
400
|
+
newMessages.push({
|
|
401
|
+
id: `assistant-${generateId()}`,
|
|
402
|
+
role: 'assistant' as const,
|
|
403
|
+
content: batch.assistantMessage,
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const update: any = { messages: newMessages };
|
|
409
|
+
if (hasThinking) {
|
|
410
|
+
update.isThinking = batch.isThinking;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
store.setState((s: any) => ({
|
|
414
|
+
session: { ...s.session, ...update },
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Cancel the current batch (e.g., on error before flush).
|
|
422
|
+
*/
|
|
423
|
+
export function cancelBatch(): void {
|
|
424
|
+
pendingBatch = null;
|
|
425
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store 模块测试
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
vanillaStore,
|
|
9
|
+
getState,
|
|
10
|
+
sessionActions,
|
|
11
|
+
configActions,
|
|
12
|
+
appActions,
|
|
13
|
+
focusActions,
|
|
14
|
+
commandActions,
|
|
15
|
+
getConfig,
|
|
16
|
+
ensureStoreInitialized,
|
|
17
|
+
subscribeToMessages,
|
|
18
|
+
} from './index.js';
|
|
19
|
+
import type { RuntimeConfig } from '../config/types.js';
|
|
20
|
+
|
|
21
|
+
let testsPassed = 0;
|
|
22
|
+
let testsFailed = 0;
|
|
23
|
+
|
|
24
|
+
function pass(msg: string) {
|
|
25
|
+
testsPassed++;
|
|
26
|
+
console.log(`✅ ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fail(msg: string, error?: any) {
|
|
30
|
+
testsFailed++;
|
|
31
|
+
console.log(`❌ ${msg}`);
|
|
32
|
+
if (error) console.log(` 错误: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runTests() {
|
|
36
|
+
console.log('='.repeat(60));
|
|
37
|
+
console.log('Store 模块测试');
|
|
38
|
+
console.log('='.repeat(60));
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
// ========== 测试 1: Store 实
|
|
42
|
+
console.log('📝 测试 1: Store 实例');
|
|
43
|
+
console.log('-'.repeat(40));
|
|
44
|
+
|
|
45
|
+
if (vanillaStore && typeof vanillaStore.getState === 'function') {
|
|
46
|
+
pass('vanillaStore 实例存在');
|
|
47
|
+
} else {
|
|
48
|
+
fail('vanillaStore 实例不存在');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const state = getState();
|
|
52
|
+
if (state.session && state.config && state.app && state.focus && state.command) {
|
|
53
|
+
pass('Store 包含所有 5 个 Slice');
|
|
54
|
+
} else {
|
|
55
|
+
fail('Store 缺少 Slice');
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
// ========== 测
|
|
60
|
+
console.log('📝 测试 2: Session Slice');
|
|
61
|
+
console.log('-'.repeat(40));
|
|
62
|
+
|
|
63
|
+
const initialSessionId = getState().session.sessionId;
|
|
64
|
+
if (initialSessionId && typeof initialSessionId === 'string') {
|
|
65
|
+
pass(`初始 sessionId: ${initialSessionId.slice(0, 20)}...`);
|
|
66
|
+
} else {
|
|
67
|
+
fail('sessionId 无效');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 添加消
|
|
71
|
+
sessionActions().addUserMessage('Hello, test!');
|
|
72
|
+
const messages = getState().session.messages;
|
|
73
|
+
if (messages.length === 1 && messages[0].content === 'Hello, test!') {
|
|
74
|
+
pass('addUserMessage 正常');
|
|
75
|
+
} else {
|
|
76
|
+
fail('addUserMessage 失败');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 设
|
|
80
|
+
sessionActions().setThinking(true);
|
|
81
|
+
if (getState().session.isThinking === true) {
|
|
82
|
+
pass('setThinking(true) 正常');
|
|
83
|
+
} else {
|
|
84
|
+
fail('setThinking 失败');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sessionActions().setThinking(false);
|
|
88
|
+
|
|
89
|
+
// 清空消
|
|
90
|
+
sessionActions().clearMessages();
|
|
91
|
+
if (getState().session.messages.length === 0) {
|
|
92
|
+
pass('clearMessages 正常');
|
|
93
|
+
} else {
|
|
94
|
+
fail('clearMessages 失败');
|
|
95
|
+
}
|
|
96
|
+
console.log();
|
|
97
|
+
|
|
98
|
+
// ========== 测
|
|
99
|
+
console.log('📝 测试 3: Config Slice');
|
|
100
|
+
console.log('-'.repeat(40));
|
|
101
|
+
|
|
102
|
+
// 初始状态 config
|
|
103
|
+
if (getConfig() === null) {
|
|
104
|
+
pass('初始 config 为 null');
|
|
105
|
+
} else {
|
|
106
|
+
fail('初始 config 不为 null');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 设置配
|
|
110
|
+
const testConfig: RuntimeConfig = {
|
|
111
|
+
default: { model: 'test-model' },
|
|
112
|
+
theme: 'dark',
|
|
113
|
+
defaultPermissionMode: 'default',
|
|
114
|
+
};
|
|
115
|
+
configActions().setConfig(testConfig);
|
|
116
|
+
|
|
117
|
+
if (getConfig()?.default?.model === 'test-model') {
|
|
118
|
+
pass('setConfig 正常');
|
|
119
|
+
} else {
|
|
120
|
+
fail('setConfig 失败');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 更新配
|
|
124
|
+
configActions().updateConfig({ theme: 'light' });
|
|
125
|
+
if (getConfig()?.theme === 'light') {
|
|
126
|
+
pass('updateConfig 正常');
|
|
127
|
+
} else {
|
|
128
|
+
fail('updateConfig 失败');
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
132
|
+
// ========== 测
|
|
133
|
+
console.log('📝 测试 4: App Slice');
|
|
134
|
+
console.log('-'.repeat(40));
|
|
135
|
+
|
|
136
|
+
if (getState().app.initializationStatus === 'pending') {
|
|
137
|
+
pass('初始 initializationStatus 为 pending');
|
|
138
|
+
} else {
|
|
139
|
+
fail('初始 initializationStatus 不正确');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
appActions().setInitializationStatus('ready');
|
|
143
|
+
if (getState().app.initializationStatus === 'ready') {
|
|
144
|
+
pass('setInitializationStatus 正常');
|
|
145
|
+
} else {
|
|
146
|
+
fail('setInitializationStatus 失败');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Todos
|
|
150
|
+
appActions().addTodo({ id: '1', title: 'Test todo', status: 'pending', createdAt: Date.now() });
|
|
151
|
+
if (getState().app.todos.length === 1) {
|
|
152
|
+
pass('addTodo 正常');
|
|
153
|
+
} else {
|
|
154
|
+
fail('addTodo 失败');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
appActions().updateTodo('1', { status: 'completed' });
|
|
158
|
+
if (getState().app.todos[0].status === 'completed') {
|
|
159
|
+
pass('updateTodo 正常');
|
|
160
|
+
} else {
|
|
161
|
+
fail('updateTodo 失败');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
appActions().removeTodo('1');
|
|
165
|
+
if (getState().app.todos.length === 0) {
|
|
166
|
+
pass('removeTodo 正常');
|
|
167
|
+
} else {
|
|
168
|
+
fail('removeTodo 失败');
|
|
169
|
+
}
|
|
170
|
+
console.log();
|
|
171
|
+
|
|
172
|
+
// ========== 测
|
|
173
|
+
console.log('📝 测试 5: Focus Slice');
|
|
174
|
+
console.log('-'.repeat(40));
|
|
175
|
+
|
|
176
|
+
if (getState().focus.currentFocus === 'input') {
|
|
177
|
+
pass('初始焦点为 input');
|
|
178
|
+
} else {
|
|
179
|
+
fail('初始焦点不正确');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
focusActions().setFocus('messages');
|
|
183
|
+
if (getState().focus.currentFocus === 'messages') {
|
|
184
|
+
pass('setFocus 正常');
|
|
185
|
+
} else {
|
|
186
|
+
fail('setFocus 失败');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (getState().focus.previousFocus === 'input') {
|
|
190
|
+
pass('previousFocus 记录正确');
|
|
191
|
+
} else {
|
|
192
|
+
fail('previousFocus 记录不正确');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
focusActions().restoreFocus();
|
|
196
|
+
if (getState().focus.currentFocus === 'input') {
|
|
197
|
+
pass('restoreFocus 正常');
|
|
198
|
+
} else {
|
|
199
|
+
fail('restoreFocus 失败');
|
|
200
|
+
}
|
|
201
|
+
console.log();
|
|
202
|
+
|
|
203
|
+
// ========== 测
|
|
204
|
+
console.log('📝 测试 6: Command Slice');
|
|
205
|
+
console.log('-'.repeat(40));
|
|
206
|
+
|
|
207
|
+
if (getState().command.isProcessing === false) {
|
|
208
|
+
pass('初始 isProcessing 为 false');
|
|
209
|
+
} else {
|
|
210
|
+
fail('初始 isProcessing 不正确');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
commandActions().setProcessing(true);
|
|
214
|
+
if (getState().command.isProcessing === true) {
|
|
215
|
+
pass('setProcessing 正常');
|
|
216
|
+
} else {
|
|
217
|
+
fail('setProcessing 失败');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 命令队
|
|
221
|
+
commandActions().enqueueCommand('command1');
|
|
222
|
+
commandActions().enqueueCommand('command2');
|
|
223
|
+
if (getState().command.pendingCommands.length === 2) {
|
|
224
|
+
pass('enqueueCommand 正常');
|
|
225
|
+
} else {
|
|
226
|
+
fail('enqueueCommand 失败');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const cmd = commandActions().dequeueCommand();
|
|
230
|
+
if (cmd === 'command1' && getState().command.pendingCommands.length === 1) {
|
|
231
|
+
pass('dequeueCommand 正常');
|
|
232
|
+
} else {
|
|
233
|
+
fail('dequeueCommand 失败');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
commandActions().clearQueue();
|
|
237
|
+
if (getState().command.pendingCommands.length === 0) {
|
|
238
|
+
pass('clearQueue 正常');
|
|
239
|
+
} else {
|
|
240
|
+
fail('clearQueue 失败');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// AbortController
|
|
244
|
+
const controller = commandActions().createAbortController();
|
|
245
|
+
if (controller instanceof AbortController) {
|
|
246
|
+
pass('createAbortController 正常');
|
|
247
|
+
} else {
|
|
248
|
+
fail('createAbortController 失败');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
commandActions().abort();
|
|
252
|
+
if (getState().command.isProcessing === false && getState().command.abortController === null) {
|
|
253
|
+
pass('abort 正常');
|
|
254
|
+
} else {
|
|
255
|
+
fail('abort 失败');
|
|
256
|
+
}
|
|
257
|
+
console.log();
|
|
258
|
+
|
|
259
|
+
// ========== 测试 7: 订阅功
|
|
260
|
+
console.log('📝 测试 7: 订阅功能');
|
|
261
|
+
console.log('-'.repeat(40));
|
|
262
|
+
|
|
263
|
+
let subscriptionTriggered = false;
|
|
264
|
+
const unsubscribe = subscribeToMessages((messages) => {
|
|
265
|
+
subscriptionTriggered = true;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
sessionActions().addUserMessage('Trigger subscription');
|
|
269
|
+
|
|
270
|
+
// 等待一下让订阅触
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
272
|
+
|
|
273
|
+
if (subscriptionTriggered) {
|
|
274
|
+
pass('subscribeToMessages 正常触发');
|
|
275
|
+
} else {
|
|
276
|
+
fail('subscribeToMessages 未触发');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
unsubscribe();
|
|
280
|
+
console.log();
|
|
281
|
+
|
|
282
|
+
// ========== 测试总
|
|
283
|
+
console.log('='.repeat(60));
|
|
284
|
+
console.log(`测试完成: ${testsPassed} 通过, ${testsFailed} 失败`);
|
|
285
|
+
console.log('='.repeat(60));
|
|
286
|
+
|
|
287
|
+
if (testsFailed > 0) {
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 运行测
|
|
293
|
+
runTests().catch((error) => {
|
|
294
|
+
console.error('测试出错:', error);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|