@ddse/acm-aicoder 0.5.0
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/.aicoder/index.json +304 -0
- package/AICODER_IMPLEMENTATION_PLAN_PHASE2.md +284 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/bin/interactive.tsx +232 -0
- package/dist/bin/interactive.d.ts +3 -0
- package/dist/bin/interactive.d.ts.map +1 -0
- package/dist/bin/interactive.js +155 -0
- package/dist/bin/interactive.js.map +1 -0
- package/dist/src/config/providers.d.ts +15 -0
- package/dist/src/config/providers.d.ts.map +1 -0
- package/dist/src/config/providers.js +142 -0
- package/dist/src/config/providers.js.map +1 -0
- package/dist/src/config/session.d.ts +25 -0
- package/dist/src/config/session.d.ts.map +1 -0
- package/dist/src/config/session.js +97 -0
- package/dist/src/config/session.js.map +1 -0
- package/dist/src/context/bm25.d.ts +68 -0
- package/dist/src/context/bm25.d.ts.map +1 -0
- package/dist/src/context/bm25.js +131 -0
- package/dist/src/context/bm25.js.map +1 -0
- package/dist/src/context/code-search.d.ts +30 -0
- package/dist/src/context/code-search.d.ts.map +1 -0
- package/dist/src/context/code-search.js +150 -0
- package/dist/src/context/code-search.js.map +1 -0
- package/dist/src/context/context-pack.d.ts +25 -0
- package/dist/src/context/context-pack.d.ts.map +1 -0
- package/dist/src/context/context-pack.js +92 -0
- package/dist/src/context/context-pack.js.map +1 -0
- package/dist/src/context/dependency-mapper.d.ts +10 -0
- package/dist/src/context/dependency-mapper.d.ts.map +1 -0
- package/dist/src/context/dependency-mapper.js +62 -0
- package/dist/src/context/dependency-mapper.js.map +1 -0
- package/dist/src/context/index.d.ts +8 -0
- package/dist/src/context/index.d.ts.map +1 -0
- package/dist/src/context/index.js +9 -0
- package/dist/src/context/index.js.map +1 -0
- package/dist/src/context/symbol-extractor.d.ts +26 -0
- package/dist/src/context/symbol-extractor.d.ts.map +1 -0
- package/dist/src/context/symbol-extractor.js +129 -0
- package/dist/src/context/symbol-extractor.js.map +1 -0
- package/dist/src/context/test-mapper.d.ts +16 -0
- package/dist/src/context/test-mapper.d.ts.map +1 -0
- package/dist/src/context/test-mapper.js +66 -0
- package/dist/src/context/test-mapper.js.map +1 -0
- package/dist/src/context/types.d.ts +61 -0
- package/dist/src/context/types.d.ts.map +1 -0
- package/dist/src/context/types.js +3 -0
- package/dist/src/context/types.js.map +1 -0
- package/dist/src/context/workspace-indexer.d.ts +39 -0
- package/dist/src/context/workspace-indexer.d.ts.map +1 -0
- package/dist/src/context/workspace-indexer.js +222 -0
- package/dist/src/context/workspace-indexer.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registries.d.ts +34 -0
- package/dist/src/registries.d.ts.map +1 -0
- package/dist/src/registries.js +87 -0
- package/dist/src/registries.js.map +1 -0
- package/dist/src/runtime/budget-manager.d.ts +42 -0
- package/dist/src/runtime/budget-manager.d.ts.map +1 -0
- package/dist/src/runtime/budget-manager.js +82 -0
- package/dist/src/runtime/budget-manager.js.map +1 -0
- package/dist/src/runtime/interactive-runtime.d.ts +39 -0
- package/dist/src/runtime/interactive-runtime.d.ts.map +1 -0
- package/dist/src/runtime/interactive-runtime.js +321 -0
- package/dist/src/runtime/interactive-runtime.js.map +1 -0
- package/dist/src/tasks-v2/analysis-tasks.d.ts +117 -0
- package/dist/src/tasks-v2/analysis-tasks.d.ts.map +1 -0
- package/dist/src/tasks-v2/analysis-tasks.js +209 -0
- package/dist/src/tasks-v2/analysis-tasks.js.map +1 -0
- package/dist/src/tasks-v2/developer-tasks.d.ts +226 -0
- package/dist/src/tasks-v2/developer-tasks.d.ts.map +1 -0
- package/dist/src/tasks-v2/developer-tasks.js +322 -0
- package/dist/src/tasks-v2/developer-tasks.js.map +1 -0
- package/dist/src/tasks-v2/index.d.ts +3 -0
- package/dist/src/tasks-v2/index.d.ts.map +1 -0
- package/dist/src/tasks-v2/index.js +4 -0
- package/dist/src/tasks-v2/index.js.map +1 -0
- package/dist/src/tools-v2/edit-tools.d.ts +67 -0
- package/dist/src/tools-v2/edit-tools.d.ts.map +1 -0
- package/dist/src/tools-v2/edit-tools.js +117 -0
- package/dist/src/tools-v2/edit-tools.js.map +1 -0
- package/dist/src/tools-v2/index.d.ts +6 -0
- package/dist/src/tools-v2/index.d.ts.map +1 -0
- package/dist/src/tools-v2/index.js +7 -0
- package/dist/src/tools-v2/index.js.map +1 -0
- package/dist/src/tools-v2/read-tools.d.ts +129 -0
- package/dist/src/tools-v2/read-tools.d.ts.map +1 -0
- package/dist/src/tools-v2/read-tools.js +216 -0
- package/dist/src/tools-v2/read-tools.js.map +1 -0
- package/dist/src/tools-v2/search-tools.d.ts +73 -0
- package/dist/src/tools-v2/search-tools.d.ts.map +1 -0
- package/dist/src/tools-v2/search-tools.js +132 -0
- package/dist/src/tools-v2/search-tools.js.map +1 -0
- package/dist/src/tools-v2/test-tools.d.ts +59 -0
- package/dist/src/tools-v2/test-tools.d.ts.map +1 -0
- package/dist/src/tools-v2/test-tools.js +111 -0
- package/dist/src/tools-v2/test-tools.js.map +1 -0
- package/dist/src/tools-v2/workspace-context.d.ts +65 -0
- package/dist/src/tools-v2/workspace-context.d.ts.map +1 -0
- package/dist/src/tools-v2/workspace-context.js +336 -0
- package/dist/src/tools-v2/workspace-context.js.map +1 -0
- package/dist/src/ui/App.d.ts +9 -0
- package/dist/src/ui/App.d.ts.map +1 -0
- package/dist/src/ui/App.js +257 -0
- package/dist/src/ui/App.js.map +1 -0
- package/dist/src/ui/components/ChatPane.d.ts +12 -0
- package/dist/src/ui/components/ChatPane.d.ts.map +1 -0
- package/dist/src/ui/components/ChatPane.js +41 -0
- package/dist/src/ui/components/ChatPane.js.map +1 -0
- package/dist/src/ui/components/EventsPane.d.ts +12 -0
- package/dist/src/ui/components/EventsPane.d.ts.map +1 -0
- package/dist/src/ui/components/EventsPane.js +48 -0
- package/dist/src/ui/components/EventsPane.js.map +1 -0
- package/dist/src/ui/components/GoalsTasksPane.d.ts +18 -0
- package/dist/src/ui/components/GoalsTasksPane.d.ts.map +1 -0
- package/dist/src/ui/components/GoalsTasksPane.js +83 -0
- package/dist/src/ui/components/GoalsTasksPane.js.map +1 -0
- package/dist/src/ui/store.d.ts +74 -0
- package/dist/src/ui/store.d.ts.map +1 -0
- package/dist/src/ui/store.js +260 -0
- package/dist/src/ui/store.js.map +1 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +415 -0
- package/dist/tests/integration.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/docs/AICODER.png +0 -0
- package/docs/INTERACTIVE_CLI_GUIDE.md +201 -0
- package/docs/TUI_MOCKUP.md +180 -0
- package/package.json +52 -0
- package/src/config/providers.ts +174 -0
- package/src/config/session.ts +143 -0
- package/src/context/bm25.ts +173 -0
- package/src/context/code-search.ts +188 -0
- package/src/context/context-pack.ts +133 -0
- package/src/context/dependency-mapper.ts +72 -0
- package/src/context/index.ts +8 -0
- package/src/context/symbol-extractor.ts +149 -0
- package/src/context/test-mapper.ts +77 -0
- package/src/context/types.ts +69 -0
- package/src/context/workspace-indexer.ts +249 -0
- package/src/index.ts +5 -0
- package/src/registries.ts +118 -0
- package/src/runtime/budget-manager.ts +118 -0
- package/src/runtime/interactive-runtime.ts +423 -0
- package/src/tasks-v2/analysis-tasks.ts +311 -0
- package/src/tasks-v2/developer-tasks.ts +437 -0
- package/src/tasks-v2/index.ts +3 -0
- package/src/tools-v2/edit-tools.ts +153 -0
- package/src/tools-v2/index.ts +6 -0
- package/src/tools-v2/read-tools.ts +286 -0
- package/src/tools-v2/search-tools.ts +175 -0
- package/src/tools-v2/test-tools.ts +147 -0
- package/src/tools-v2/workspace-context.ts +428 -0
- package/src/ui/App.tsx +392 -0
- package/src/ui/components/ChatPane.tsx +84 -0
- package/src/ui/components/EventsPane.tsx +81 -0
- package/src/ui/components/GoalsTasksPane.tsx +149 -0
- package/src/ui/store.ts +362 -0
- package/tests/integration.test.ts +537 -0
- package/tsconfig.json +22 -0
package/src/ui/App.tsx
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Main TUI Application Component
|
|
2
|
+
// Three-column layout with command input
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
5
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
6
|
+
import TextInput from 'ink-text-input';
|
|
7
|
+
import { ChatPane } from './components/ChatPane.js';
|
|
8
|
+
import { GoalsTasksPane } from './components/GoalsTasksPane.js';
|
|
9
|
+
import { EventsPane } from './components/EventsPane.js';
|
|
10
|
+
import type { AppStore } from './store.js';
|
|
11
|
+
|
|
12
|
+
type Pane = 'chat' | 'tasks' | 'events';
|
|
13
|
+
|
|
14
|
+
interface WindowMeta {
|
|
15
|
+
startIndex: number;
|
|
16
|
+
endIndex: number;
|
|
17
|
+
clampedOffset: number;
|
|
18
|
+
maxOffset: number;
|
|
19
|
+
capacity: number;
|
|
20
|
+
canScrollUp: boolean;
|
|
21
|
+
canScrollDown: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const paneLabels: Record<Pane, string> = {
|
|
25
|
+
chat: 'Chat',
|
|
26
|
+
tasks: 'Goal/Tasks',
|
|
27
|
+
events: 'Events',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const paneOrder: Pane[] = ['chat', 'tasks', 'events'];
|
|
31
|
+
|
|
32
|
+
const computeWindow = (total: number, capacity: number, requestedOffset: number): WindowMeta => {
|
|
33
|
+
const normalizedCapacity = Math.max(1, capacity);
|
|
34
|
+
const maxOffset = Math.max(0, total - normalizedCapacity);
|
|
35
|
+
const clampedOffset = Math.min(Math.max(requestedOffset, 0), maxOffset);
|
|
36
|
+
const startIndex = Math.max(0, total - normalizedCapacity - clampedOffset);
|
|
37
|
+
const endIndex = Math.min(total, startIndex + normalizedCapacity);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
startIndex,
|
|
41
|
+
endIndex,
|
|
42
|
+
clampedOffset,
|
|
43
|
+
maxOffset,
|
|
44
|
+
capacity: normalizedCapacity,
|
|
45
|
+
canScrollUp: clampedOffset < maxOffset,
|
|
46
|
+
canScrollDown: clampedOffset > 0,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface AppProps {
|
|
51
|
+
store: AppStore;
|
|
52
|
+
onCommand: (command: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const App: React.FC<AppProps> = ({ store, onCommand }) => {
|
|
56
|
+
const [state, setState] = useState(store.getState());
|
|
57
|
+
const [input, setInput] = useState('');
|
|
58
|
+
const [focusedPane, setFocusedPane] = useState<Pane>('chat');
|
|
59
|
+
const [scrollOffsets, setScrollOffsets] = useState<{ chat: number; tasks: number; events: number }>(
|
|
60
|
+
{ chat: 0, tasks: 0, events: 0 }
|
|
61
|
+
);
|
|
62
|
+
const { exit } = useApp();
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const handleUpdate = () => {
|
|
66
|
+
setState(store.getState());
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
store.on('update', handleUpdate);
|
|
70
|
+
return () => {
|
|
71
|
+
store.off('update', handleUpdate);
|
|
72
|
+
};
|
|
73
|
+
}, [store]);
|
|
74
|
+
|
|
75
|
+
const handleSubmit = (value: string) => {
|
|
76
|
+
if (!value.trim()) return;
|
|
77
|
+
|
|
78
|
+
// Handle built-in commands
|
|
79
|
+
if (value === '/exit' || value === '/quit') {
|
|
80
|
+
exit();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (value === '/help') {
|
|
85
|
+
store.addMessage('system', `
|
|
86
|
+
Available commands:
|
|
87
|
+
/exit, /quit - Exit the application
|
|
88
|
+
/help - Show this help
|
|
89
|
+
/retry - Retry the last failed task
|
|
90
|
+
/skip - Skip the current task
|
|
91
|
+
/context - Show current context info
|
|
92
|
+
/budget - Show token budget details
|
|
93
|
+
/reset - Reset session (clear goal and tasks)
|
|
94
|
+
/reasoning - Toggle nucleus reasoning messages
|
|
95
|
+
|
|
96
|
+
Pane controls:
|
|
97
|
+
Tab - Switch focused column
|
|
98
|
+
Ctrl+↑ / Ctrl+↓ - Scroll active column
|
|
99
|
+
PgUp / PgDn - Page through active column
|
|
100
|
+
Ctrl+C - Exit immediately
|
|
101
|
+
|
|
102
|
+
File mentions:
|
|
103
|
+
Use #path/to/file to reference files in your workspace
|
|
104
|
+
|
|
105
|
+
Type your goal or request to start planning.
|
|
106
|
+
`.trim());
|
|
107
|
+
setInput('');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (value === '/reset') {
|
|
112
|
+
store.reset();
|
|
113
|
+
setInput('');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (value === '/budget' && state.budgetStatus) {
|
|
118
|
+
const bs = state.budgetStatus;
|
|
119
|
+
store.addMessage('system',
|
|
120
|
+
`Token Usage:\n` +
|
|
121
|
+
` Total Tokens: ${bs.totalTokens}\n` +
|
|
122
|
+
` Input Tokens: ${bs.totalInputTokens}\n` +
|
|
123
|
+
` Output Tokens: ${bs.totalOutputTokens}\n` +
|
|
124
|
+
(bs.maxTokens !== undefined ? ` Allowance: ${bs.maxTokens}\n` : '') +
|
|
125
|
+
(bs.remainingTokens !== undefined ? ` Remaining: ${bs.remainingTokens}\n` : '') +
|
|
126
|
+
` API Calls: ${bs.callCount}`
|
|
127
|
+
);
|
|
128
|
+
setInput('');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (value === '/context' && state.currentContext) {
|
|
133
|
+
store.addMessage('system',
|
|
134
|
+
`Current Context:\n` +
|
|
135
|
+
` ID: ${state.currentContext.id}\n` +
|
|
136
|
+
` Facts: ${Object.keys(state.currentContext.facts || {}).length} entries`
|
|
137
|
+
);
|
|
138
|
+
setInput('');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (value === '/reasoning') {
|
|
143
|
+
const enabled = store.toggleReasoningVisible();
|
|
144
|
+
store.addMessage(
|
|
145
|
+
'system',
|
|
146
|
+
`Nucleus reasoning messages ${enabled ? 'enabled' : 'hidden'} (Ctrl+R to toggle).`
|
|
147
|
+
);
|
|
148
|
+
setInput('');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Pass command to handler
|
|
153
|
+
onCommand(value);
|
|
154
|
+
setInput('');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Calculate terminal dimensions (approximate)
|
|
158
|
+
const termHeight = process.stdout.rows || 24;
|
|
159
|
+
const termWidth = process.stdout.columns || 80;
|
|
160
|
+
|
|
161
|
+
const mainHeight = termHeight - 4; // Leave room for input
|
|
162
|
+
const col1Width = Math.floor(termWidth * 0.40);
|
|
163
|
+
const col2Width = Math.floor(termWidth * 0.30);
|
|
164
|
+
const col3Width = termWidth - col1Width - col2Width - 4;
|
|
165
|
+
|
|
166
|
+
const visibleMessages = useMemo(
|
|
167
|
+
() =>
|
|
168
|
+
state.showReasoning
|
|
169
|
+
? state.messages
|
|
170
|
+
: state.messages.filter(message => message.role !== 'nucleus'),
|
|
171
|
+
[state.messages, state.showReasoning]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const chatCapacity = Math.max(1, mainHeight - 2);
|
|
175
|
+
const chatWindow = useMemo(
|
|
176
|
+
() => computeWindow(visibleMessages.length, chatCapacity, scrollOffsets.chat),
|
|
177
|
+
[visibleMessages.length, chatCapacity, scrollOffsets.chat]
|
|
178
|
+
);
|
|
179
|
+
const chatMessages = useMemo(
|
|
180
|
+
() => visibleMessages.slice(chatWindow.startIndex, chatWindow.endIndex),
|
|
181
|
+
[visibleMessages, chatWindow]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const tasksTotalEntries = useMemo(() => {
|
|
185
|
+
const goalEntry = state.currentGoal ? 1 : 0;
|
|
186
|
+
const summaryEntry = state.goalSummary ? 1 : 0;
|
|
187
|
+
const tasksHeader = state.tasks.length > 0 ? 1 : 0;
|
|
188
|
+
const taskEntries = state.tasks.length;
|
|
189
|
+
const contextHeader = state.currentContext ? 1 : 0;
|
|
190
|
+
const contextLines = state.currentContext ? 2 : 0;
|
|
191
|
+
const placeholderEntry = state.currentGoal ? 0 : 1;
|
|
192
|
+
return (
|
|
193
|
+
goalEntry +
|
|
194
|
+
summaryEntry +
|
|
195
|
+
tasksHeader +
|
|
196
|
+
taskEntries +
|
|
197
|
+
contextHeader +
|
|
198
|
+
contextLines +
|
|
199
|
+
placeholderEntry
|
|
200
|
+
);
|
|
201
|
+
}, [state.currentGoal, state.goalSummary, state.tasks, state.currentContext]);
|
|
202
|
+
const tasksCapacity = Math.max(1, mainHeight - 2);
|
|
203
|
+
const tasksWindow = useMemo(
|
|
204
|
+
() => computeWindow(tasksTotalEntries, tasksCapacity, scrollOffsets.tasks),
|
|
205
|
+
[tasksTotalEntries, tasksCapacity, scrollOffsets.tasks]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const eventsCapacity = Math.max(1, mainHeight - 2);
|
|
209
|
+
const eventsWindow = useMemo(
|
|
210
|
+
() => computeWindow(state.events.length, eventsCapacity, scrollOffsets.events),
|
|
211
|
+
[state.events.length, eventsCapacity, scrollOffsets.events]
|
|
212
|
+
);
|
|
213
|
+
const eventEntries = useMemo(
|
|
214
|
+
() => state.events.slice(eventsWindow.startIndex, eventsWindow.endIndex),
|
|
215
|
+
[state.events, eventsWindow]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const windowByPane = useMemo(
|
|
219
|
+
() => ({ chat: chatWindow, tasks: tasksWindow, events: eventsWindow }),
|
|
220
|
+
[chatWindow, tasksWindow, eventsWindow]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (scrollOffsets.chat !== chatWindow.clampedOffset) {
|
|
225
|
+
setScrollOffsets(prev =>
|
|
226
|
+
prev.chat === chatWindow.clampedOffset
|
|
227
|
+
? prev
|
|
228
|
+
: { ...prev, chat: chatWindow.clampedOffset }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}, [chatWindow.clampedOffset, scrollOffsets.chat]);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (scrollOffsets.tasks !== tasksWindow.clampedOffset) {
|
|
235
|
+
setScrollOffsets(prev =>
|
|
236
|
+
prev.tasks === tasksWindow.clampedOffset
|
|
237
|
+
? prev
|
|
238
|
+
: { ...prev, tasks: tasksWindow.clampedOffset }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}, [tasksWindow.clampedOffset, scrollOffsets.tasks]);
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (scrollOffsets.events !== eventsWindow.clampedOffset) {
|
|
245
|
+
setScrollOffsets(prev =>
|
|
246
|
+
prev.events === eventsWindow.clampedOffset
|
|
247
|
+
? prev
|
|
248
|
+
: { ...prev, events: eventsWindow.clampedOffset }
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}, [eventsWindow.clampedOffset, scrollOffsets.events]);
|
|
252
|
+
|
|
253
|
+
const setPaneOffset = useCallback(
|
|
254
|
+
(pane: Pane, offset: number) => {
|
|
255
|
+
const meta = windowByPane[pane];
|
|
256
|
+
if (!meta) return;
|
|
257
|
+
const clamped = Math.min(Math.max(offset, 0), meta.maxOffset);
|
|
258
|
+
setScrollOffsets(prev =>
|
|
259
|
+
prev[pane] === clamped ? prev : { ...prev, [pane]: clamped }
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
[windowByPane]
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const adjustScroll = useCallback(
|
|
266
|
+
(pane: Pane, delta: number) => {
|
|
267
|
+
const meta = windowByPane[pane];
|
|
268
|
+
if (!meta) return;
|
|
269
|
+
setPaneOffset(pane, meta.clampedOffset + delta);
|
|
270
|
+
},
|
|
271
|
+
[setPaneOffset, windowByPane]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
useInput((inputKey, key) => {
|
|
275
|
+
if (key.tab) {
|
|
276
|
+
setFocusedPane(prev => {
|
|
277
|
+
const idx = paneOrder.indexOf(prev);
|
|
278
|
+
const nextIdx = key.shift
|
|
279
|
+
? (idx - 1 + paneOrder.length) % paneOrder.length
|
|
280
|
+
: (idx + 1) % paneOrder.length;
|
|
281
|
+
return paneOrder[nextIdx];
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (key.ctrl && inputKey && inputKey.toLowerCase() === 'r') {
|
|
287
|
+
const enabled = store.toggleReasoningVisible();
|
|
288
|
+
store.addMessage(
|
|
289
|
+
'system',
|
|
290
|
+
`Nucleus reasoning messages ${enabled ? 'enabled' : 'hidden'} (Ctrl+R to toggle).`
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const meta = windowByPane[focusedPane];
|
|
296
|
+
if (!meta) return;
|
|
297
|
+
|
|
298
|
+
const lineScrollModifier = key.ctrl || key.meta || (('alt' in key) && (key as any).alt);
|
|
299
|
+
|
|
300
|
+
if ((lineScrollModifier || key.shift) && key.upArrow) {
|
|
301
|
+
adjustScroll(focusedPane, 1);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if ((lineScrollModifier || key.shift) && key.downArrow) {
|
|
306
|
+
adjustScroll(focusedPane, -1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (key.pageUp || (key.shift && key.upArrow)) {
|
|
311
|
+
adjustScroll(focusedPane, meta.capacity);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (key.pageDown || (key.shift && key.downArrow)) {
|
|
316
|
+
adjustScroll(focusedPane, -meta.capacity);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<Box flexDirection="column" height={termHeight}>
|
|
323
|
+
{/* Header */}
|
|
324
|
+
<Box borderStyle="double" borderColor="cyan" paddingX={1}>
|
|
325
|
+
<Text bold color="cyan">ACM AI Coder - Interactive Mode</Text>
|
|
326
|
+
<Box marginLeft={2}>
|
|
327
|
+
<Text color="cyan">Focus: {paneLabels[focusedPane]}</Text>
|
|
328
|
+
<Text>
|
|
329
|
+
{' '}
|
|
330
|
+
<Text color="gray">Tab</Text> = Switch
|
|
331
|
+
{' '}
|
|
332
|
+
<Text color="gray">Shift+Tab</Text> = Reverse
|
|
333
|
+
{' '}
|
|
334
|
+
<Text color="gray">Ctrl+R</Text> = Reasoning Toggle
|
|
335
|
+
{' '}
|
|
336
|
+
<Text color="gray">Ctrl+↑/↓</Text> = Scroll
|
|
337
|
+
{' '}
|
|
338
|
+
<Text color="gray">PgUp/PgDn</Text> = Page
|
|
339
|
+
{' '}
|
|
340
|
+
<Text color="gray">Ctrl+C</Text> = Exit
|
|
341
|
+
</Text>
|
|
342
|
+
</Box>
|
|
343
|
+
</Box>
|
|
344
|
+
|
|
345
|
+
<Box flexGrow={1} flexDirection="row" gap={2} paddingX={1}>
|
|
346
|
+
<Box width={col1Width}>
|
|
347
|
+
<ChatPane
|
|
348
|
+
messages={chatMessages}
|
|
349
|
+
height={mainHeight}
|
|
350
|
+
canScrollUp={chatWindow.canScrollUp}
|
|
351
|
+
canScrollDown={chatWindow.canScrollDown}
|
|
352
|
+
focused={focusedPane === 'chat'}
|
|
353
|
+
/>
|
|
354
|
+
</Box>
|
|
355
|
+
<Box width={col2Width}>
|
|
356
|
+
<GoalsTasksPane
|
|
357
|
+
goal={state.currentGoal}
|
|
358
|
+
plan={state.currentPlan}
|
|
359
|
+
context={state.currentContext}
|
|
360
|
+
tasks={state.tasks}
|
|
361
|
+
goalSummary={state.goalSummary}
|
|
362
|
+
height={mainHeight}
|
|
363
|
+
scrollOffset={tasksWindow.clampedOffset}
|
|
364
|
+
canScrollUp={tasksWindow.canScrollUp}
|
|
365
|
+
canScrollDown={tasksWindow.canScrollDown}
|
|
366
|
+
focused={focusedPane === 'tasks'}
|
|
367
|
+
/>
|
|
368
|
+
</Box>
|
|
369
|
+
<Box width={col3Width}>
|
|
370
|
+
<EventsPane
|
|
371
|
+
events={eventEntries}
|
|
372
|
+
height={mainHeight}
|
|
373
|
+
canScrollUp={eventsWindow.canScrollUp}
|
|
374
|
+
canScrollDown={eventsWindow.canScrollDown}
|
|
375
|
+
focused={focusedPane === 'events'}
|
|
376
|
+
/>
|
|
377
|
+
</Box>
|
|
378
|
+
</Box>
|
|
379
|
+
|
|
380
|
+
{/* Command input */}
|
|
381
|
+
<Box borderStyle="single" borderColor="cyan" paddingX={1}>
|
|
382
|
+
<Text color="cyan" bold>> </Text>
|
|
383
|
+
<TextInput
|
|
384
|
+
value={input}
|
|
385
|
+
onChange={setInput}
|
|
386
|
+
onSubmit={handleSubmit}
|
|
387
|
+
placeholder="Type your goal or command (e.g., /help)..."
|
|
388
|
+
/>
|
|
389
|
+
</Box>
|
|
390
|
+
</Box>
|
|
391
|
+
);
|
|
392
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Chat Pane Component
|
|
2
|
+
// Displays user messages, planner reasoning, and nucleus thoughts
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
import type { ChatMessage } from '../store.js';
|
|
7
|
+
|
|
8
|
+
interface ChatPaneProps {
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
height: number;
|
|
11
|
+
canScrollUp: boolean;
|
|
12
|
+
canScrollDown: boolean;
|
|
13
|
+
focused: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ChatPane: React.FC<ChatPaneProps> = ({
|
|
17
|
+
messages,
|
|
18
|
+
height,
|
|
19
|
+
canScrollUp,
|
|
20
|
+
canScrollDown,
|
|
21
|
+
focused,
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box
|
|
26
|
+
flexDirection="column"
|
|
27
|
+
height={height}
|
|
28
|
+
borderStyle="single"
|
|
29
|
+
borderColor={focused ? 'white' : 'cyan'}
|
|
30
|
+
>
|
|
31
|
+
<Box paddingX={1} borderStyle="single" borderColor={focused ? 'white' : 'cyan'}>
|
|
32
|
+
<Text bold color={focused ? 'white' : 'cyan'}>Chat</Text>
|
|
33
|
+
<Box flexGrow={1} />
|
|
34
|
+
<ScrollIndicator
|
|
35
|
+
up={canScrollUp}
|
|
36
|
+
down={canScrollDown}
|
|
37
|
+
color={focused ? 'white' : 'cyan'}
|
|
38
|
+
/>
|
|
39
|
+
</Box>
|
|
40
|
+
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
41
|
+
{messages.length === 0 && (
|
|
42
|
+
<Text color="gray">Conversation will appear here.</Text>
|
|
43
|
+
)}
|
|
44
|
+
{messages.map(msg => (
|
|
45
|
+
<ChatMessageItem key={msg.id} message={msg} />
|
|
46
|
+
))}
|
|
47
|
+
</Box>
|
|
48
|
+
</Box>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ChatMessageItem: React.FC<{ message: ChatMessage }> = ({ message }) => {
|
|
53
|
+
const roleColors = {
|
|
54
|
+
user: 'white',
|
|
55
|
+
planner: 'yellow',
|
|
56
|
+
nucleus: 'magenta',
|
|
57
|
+
system: 'gray',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const roleLabels = {
|
|
61
|
+
user: 'You',
|
|
62
|
+
planner: 'Planner',
|
|
63
|
+
nucleus: 'Nucleus',
|
|
64
|
+
system: 'System',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const color = roleColors[message.role] || 'white';
|
|
68
|
+
const label = roleLabels[message.role] || message.role;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Box flexDirection="column" marginY={0}>
|
|
72
|
+
<Text color={color} bold>
|
|
73
|
+
{label}{message.streaming ? ' (streaming...)' : ''}:
|
|
74
|
+
</Text>
|
|
75
|
+
<Text color={color}>
|
|
76
|
+
{message.content || '...'}
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ScrollIndicator: React.FC<{ up: boolean; down: boolean; color: string }> = ({ up, down, color }) => (
|
|
83
|
+
<Text color={color}>{up ? '˄' : ' '} {down ? '˅' : ' '}</Text>
|
|
84
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Event Stream Pane Component
|
|
2
|
+
// Shows ledger events, tool calls, context updates, policy decisions
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
import type { EventEntry } from '../store.js';
|
|
7
|
+
|
|
8
|
+
interface EventsPaneProps {
|
|
9
|
+
events: EventEntry[];
|
|
10
|
+
height: number;
|
|
11
|
+
canScrollUp: boolean;
|
|
12
|
+
canScrollDown: boolean;
|
|
13
|
+
focused: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const EventsPane: React.FC<EventsPaneProps> = ({
|
|
17
|
+
events,
|
|
18
|
+
height,
|
|
19
|
+
canScrollUp,
|
|
20
|
+
canScrollDown,
|
|
21
|
+
focused,
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box
|
|
26
|
+
flexDirection="column"
|
|
27
|
+
height={height}
|
|
28
|
+
borderStyle="single"
|
|
29
|
+
borderColor={focused ? 'white' : 'blue'}
|
|
30
|
+
>
|
|
31
|
+
<Box paddingX={1} borderStyle="single" borderColor={focused ? 'white' : 'blue'}>
|
|
32
|
+
<Text bold color={focused ? 'white' : 'blue'}>Event Stream</Text>
|
|
33
|
+
<Box flexGrow={1} />
|
|
34
|
+
<ScrollIndicator
|
|
35
|
+
up={canScrollUp}
|
|
36
|
+
down={canScrollDown}
|
|
37
|
+
color={focused ? 'white' : 'blue'}
|
|
38
|
+
/>
|
|
39
|
+
</Box>
|
|
40
|
+
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
41
|
+
{events.map(event => (
|
|
42
|
+
<EventItem key={event.id} event={event} />
|
|
43
|
+
))}
|
|
44
|
+
{events.length === 0 && (
|
|
45
|
+
<Text color="gray">No events yet. Events will appear here as actions occur.</Text>
|
|
46
|
+
)}
|
|
47
|
+
</Box>
|
|
48
|
+
</Box>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const EventItem: React.FC<{ event: EventEntry }> = ({ event }) => {
|
|
53
|
+
const color = event.color || 'white';
|
|
54
|
+
|
|
55
|
+
// Format event data for display
|
|
56
|
+
const formatData = (data: any): string => {
|
|
57
|
+
if (typeof data === 'string') return data;
|
|
58
|
+
if (typeof data === 'object') {
|
|
59
|
+
// Show a summary for objects
|
|
60
|
+
if (data.message) return data.message;
|
|
61
|
+
if (data.status) return `status: ${data.status}`;
|
|
62
|
+
if (data.name) return data.name;
|
|
63
|
+
return JSON.stringify(data).slice(0, 50);
|
|
64
|
+
}
|
|
65
|
+
return String(data);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const timestamp = new Date(event.timestamp).toLocaleTimeString();
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Box>
|
|
72
|
+
<Text color="gray">[{timestamp}] </Text>
|
|
73
|
+
<Text color={color} bold>{event.type}</Text>
|
|
74
|
+
<Text>: {formatData(event.data)}</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ScrollIndicator: React.FC<{ up: boolean; down: boolean; color: string }> = ({ up, down, color }) => (
|
|
80
|
+
<Text color={color}>{up ? '˄' : ' '} {down ? '˅' : ' '}</Text>
|
|
81
|
+
);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Goals and Tasks Pane Component
|
|
2
|
+
// Shows current goal, plan, tasks with status, and budget info
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Box, Text } from 'ink';
|
|
6
|
+
import type { Goal, Plan, Context } from '@ddse/acm-sdk';
|
|
7
|
+
import type { TaskState } from '../store.js';
|
|
8
|
+
|
|
9
|
+
interface GoalsTasksPaneProps {
|
|
10
|
+
goal?: Goal;
|
|
11
|
+
plan?: Plan;
|
|
12
|
+
context?: Context;
|
|
13
|
+
tasks: TaskState[];
|
|
14
|
+
goalSummary?: string;
|
|
15
|
+
height: number;
|
|
16
|
+
scrollOffset: number;
|
|
17
|
+
canScrollUp: boolean;
|
|
18
|
+
canScrollDown: boolean;
|
|
19
|
+
focused: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const GoalsTasksPane: React.FC<GoalsTasksPaneProps> = ({
|
|
23
|
+
goal,
|
|
24
|
+
plan,
|
|
25
|
+
context,
|
|
26
|
+
tasks,
|
|
27
|
+
goalSummary,
|
|
28
|
+
height,
|
|
29
|
+
scrollOffset,
|
|
30
|
+
canScrollUp,
|
|
31
|
+
canScrollDown,
|
|
32
|
+
focused,
|
|
33
|
+
}) => {
|
|
34
|
+
const rows: React.ReactNode[] = [];
|
|
35
|
+
|
|
36
|
+
if (goal) {
|
|
37
|
+
rows.push(
|
|
38
|
+
<Box flexDirection="column" marginBottom={1} key="goal">
|
|
39
|
+
<Text bold color="green">Goal:</Text>
|
|
40
|
+
<Text>{goal.intent || goal.id}</Text>
|
|
41
|
+
{goalSummary && (
|
|
42
|
+
<Box marginTop={1} flexDirection="column">
|
|
43
|
+
<Text bold color="cyan">Summary</Text>
|
|
44
|
+
<Text color="cyan">{goalSummary}</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
)}
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (tasks.length > 0) {
|
|
52
|
+
rows.push(
|
|
53
|
+
<Box flexDirection="column" marginBottom={1} key="tasks-header">
|
|
54
|
+
<Text bold color="green">Tasks:</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
tasks.forEach(task => {
|
|
59
|
+
rows.push(<TaskItem key={task.id} task={task} />);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (context) {
|
|
64
|
+
const factsCount = Object.keys(context.facts ?? {}).length;
|
|
65
|
+
const augmentationCount = context.augmentations?.length ?? 0;
|
|
66
|
+
const augmentationLabel = augmentationCount === 1 ? 'time' : 'times';
|
|
67
|
+
|
|
68
|
+
rows.push(
|
|
69
|
+
<Box flexDirection="column" marginBottom={1} key="context-info">
|
|
70
|
+
<Text bold color="green">Context Information:</Text>
|
|
71
|
+
<Text color="gray"> - current size: {factsCount}</Text>
|
|
72
|
+
<Text color="gray"> - augmented {augmentationCount} {augmentationLabel}</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!goal) {
|
|
78
|
+
rows.push(
|
|
79
|
+
<Box key="no-goal">
|
|
80
|
+
<Text color="gray">No active goal. Type a command to start.</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const capacity = Math.max(1, height - 2);
|
|
86
|
+
const maxOffset = Math.max(0, rows.length - capacity);
|
|
87
|
+
const effectiveOffset = Math.min(Math.max(scrollOffset, 0), maxOffset);
|
|
88
|
+
const start = Math.max(0, rows.length - capacity - effectiveOffset);
|
|
89
|
+
const visibleRows = rows.slice(start, start + capacity);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Box
|
|
93
|
+
flexDirection="column"
|
|
94
|
+
height={height}
|
|
95
|
+
borderStyle="single"
|
|
96
|
+
borderColor={focused ? 'white' : 'green'}
|
|
97
|
+
>
|
|
98
|
+
<Box paddingX={1} borderStyle="single" borderColor={focused ? 'white' : 'green'}>
|
|
99
|
+
<Text bold color={focused ? 'white' : 'green'}>Goal / Tasks / Progress</Text>
|
|
100
|
+
<Box flexGrow={1} />
|
|
101
|
+
<ScrollIndicator
|
|
102
|
+
up={canScrollUp}
|
|
103
|
+
down={canScrollDown}
|
|
104
|
+
color={focused ? 'white' : 'green'}
|
|
105
|
+
/>
|
|
106
|
+
</Box>
|
|
107
|
+
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
108
|
+
{visibleRows}
|
|
109
|
+
</Box>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const TaskItem: React.FC<{ task: TaskState }> = ({ task }) => {
|
|
115
|
+
const statusIcons = {
|
|
116
|
+
pending: '○',
|
|
117
|
+
running: '◐',
|
|
118
|
+
succeeded: '●',
|
|
119
|
+
failed: '✗',
|
|
120
|
+
retrying: '⟳',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const statusColors = {
|
|
124
|
+
pending: 'gray',
|
|
125
|
+
running: 'yellow',
|
|
126
|
+
succeeded: 'green',
|
|
127
|
+
failed: 'red',
|
|
128
|
+
retrying: 'yellow',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const icon = statusIcons[task.status] || '○';
|
|
132
|
+
const color = statusColors[task.status] || 'white';
|
|
133
|
+
|
|
134
|
+
const label = (task.objective && task.objective.trim().length > 0)
|
|
135
|
+
? task.objective.trim()
|
|
136
|
+
: (task.title ?? task.name);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Box>
|
|
140
|
+
<Text color={color}>
|
|
141
|
+
{icon} {label}
|
|
142
|
+
</Text>
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const ScrollIndicator: React.FC<{ up: boolean; down: boolean; color: string }> = ({ up, down, color }) => (
|
|
148
|
+
<Text color={color}>{up ? '˄' : ' '} {down ? '˅' : ' '}</Text>
|
|
149
|
+
);
|