snow-ai 0.3.31 → 0.3.32

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.
@@ -32,6 +32,9 @@ import { convertSessionMessagesToUI } from '../../utils/sessionConverter.js';
32
32
  import { incrementalSnapshotManager } from '../../utils/incrementalSnapshot.js';
33
33
  import { formatElapsedTime } from '../../utils/textUtils.js';
34
34
  import { shouldAutoCompress, performAutoCompression, } from '../../utils/autoCompress.js';
35
+ import { CodebaseIndexAgent } from '../../agents/codebaseIndexAgent.js';
36
+ import { loadCodebaseConfig } from '../../utils/codebaseConfig.js';
37
+ import { logger } from '../../utils/logger.js';
35
38
  // Import commands to register them
36
39
  import '../../utils/commands/clear.js';
37
40
  import '../../utils/commands/resume.js';
@@ -85,6 +88,12 @@ export default function ChatScreen({ skipWelcome }) {
85
88
  const { stdout } = useStdout();
86
89
  const workingDirectory = process.cwd();
87
90
  const isInitialMount = useRef(true);
91
+ // Codebase indexing state
92
+ const [codebaseIndexing, setCodebaseIndexing] = useState(false);
93
+ const [codebaseProgress, setCodebaseProgress] = useState(null);
94
+ const [watcherEnabled, setWatcherEnabled] = useState(false);
95
+ const [fileUpdateNotification, setFileUpdateNotification] = useState(null);
96
+ const codebaseAgentRef = useRef(null);
88
97
  // Use custom hooks
89
98
  const streamingState = useStreamingState();
90
99
  const vscodeState = useVSCodeState();
@@ -95,6 +104,154 @@ export default function ChatScreen({ skipWelcome }) {
95
104
  useEffect(() => {
96
105
  pendingMessagesRef.current = pendingMessages;
97
106
  }, [pendingMessages]);
107
+ // Auto-start codebase indexing on mount if enabled
108
+ useEffect(() => {
109
+ const startCodebaseIndexing = async () => {
110
+ try {
111
+ const config = loadCodebaseConfig();
112
+ // Only start if enabled and not already indexing
113
+ if (!config.enabled || codebaseIndexing) {
114
+ return;
115
+ }
116
+ // Initialize agent
117
+ const agent = new CodebaseIndexAgent(workingDirectory);
118
+ codebaseAgentRef.current = agent;
119
+ // Check if indexing is needed
120
+ const progress = agent.getProgress();
121
+ // If indexing is already completed, start watcher and return early
122
+ if (progress.status === 'completed' && progress.totalChunks > 0) {
123
+ agent.startWatching(progressData => {
124
+ setCodebaseProgress({
125
+ totalFiles: progressData.totalFiles,
126
+ processedFiles: progressData.processedFiles,
127
+ totalChunks: progressData.totalChunks,
128
+ currentFile: progressData.currentFile,
129
+ status: progressData.status,
130
+ });
131
+ // Handle file update notifications
132
+ if (progressData.totalFiles === 0 && progressData.currentFile) {
133
+ setFileUpdateNotification({
134
+ file: progressData.currentFile,
135
+ timestamp: Date.now(),
136
+ });
137
+ // Clear notification after 3 seconds
138
+ setTimeout(() => {
139
+ setFileUpdateNotification(null);
140
+ }, 3000);
141
+ }
142
+ });
143
+ setWatcherEnabled(true);
144
+ return;
145
+ }
146
+ // If watcher was enabled before but indexing not completed, restore it
147
+ const wasWatcherEnabled = agent.isWatcherEnabled();
148
+ if (wasWatcherEnabled) {
149
+ logger.info('Restoring file watcher from previous session');
150
+ agent.startWatching(progressData => {
151
+ setCodebaseProgress({
152
+ totalFiles: progressData.totalFiles,
153
+ processedFiles: progressData.processedFiles,
154
+ totalChunks: progressData.totalChunks,
155
+ currentFile: progressData.currentFile,
156
+ status: progressData.status,
157
+ });
158
+ // Handle file update notifications
159
+ if (progressData.totalFiles === 0 && progressData.currentFile) {
160
+ setFileUpdateNotification({
161
+ file: progressData.currentFile,
162
+ timestamp: Date.now(),
163
+ });
164
+ // Clear notification after 3 seconds
165
+ setTimeout(() => {
166
+ setFileUpdateNotification(null);
167
+ }, 3000);
168
+ }
169
+ });
170
+ setWatcherEnabled(true);
171
+ }
172
+ // Start or resume indexing in background
173
+ setCodebaseIndexing(true);
174
+ agent.start(progressData => {
175
+ setCodebaseProgress({
176
+ totalFiles: progressData.totalFiles,
177
+ processedFiles: progressData.processedFiles,
178
+ totalChunks: progressData.totalChunks,
179
+ currentFile: progressData.currentFile,
180
+ status: progressData.status,
181
+ });
182
+ // Handle file update notifications (when totalFiles is 0, it's a file update)
183
+ if (progressData.totalFiles === 0 && progressData.currentFile) {
184
+ setFileUpdateNotification({
185
+ file: progressData.currentFile,
186
+ timestamp: Date.now(),
187
+ });
188
+ // Clear notification after 3 seconds
189
+ setTimeout(() => {
190
+ setFileUpdateNotification(null);
191
+ }, 3000);
192
+ }
193
+ // Stop indexing when completed or error
194
+ if (progressData.status === 'completed' ||
195
+ progressData.status === 'error') {
196
+ setCodebaseIndexing(false);
197
+ // Start file watcher after initial indexing is completed
198
+ if (progressData.status === 'completed' && agent) {
199
+ agent.startWatching(watcherProgressData => {
200
+ setCodebaseProgress({
201
+ totalFiles: watcherProgressData.totalFiles,
202
+ processedFiles: watcherProgressData.processedFiles,
203
+ totalChunks: watcherProgressData.totalChunks,
204
+ currentFile: watcherProgressData.currentFile,
205
+ status: watcherProgressData.status,
206
+ });
207
+ // Handle file update notifications
208
+ if (watcherProgressData.totalFiles === 0 &&
209
+ watcherProgressData.currentFile) {
210
+ setFileUpdateNotification({
211
+ file: watcherProgressData.currentFile,
212
+ timestamp: Date.now(),
213
+ });
214
+ // Clear notification after 3 seconds
215
+ setTimeout(() => {
216
+ setFileUpdateNotification(null);
217
+ }, 3000);
218
+ }
219
+ });
220
+ setWatcherEnabled(true);
221
+ }
222
+ }
223
+ });
224
+ }
225
+ catch (error) {
226
+ console.error('Failed to start codebase indexing:', error);
227
+ setCodebaseIndexing(false);
228
+ }
229
+ };
230
+ startCodebaseIndexing();
231
+ // Cleanup on unmount - just stop indexing, don't close database
232
+ // This allows resuming when returning to chat screen
233
+ return () => {
234
+ if (codebaseAgentRef.current) {
235
+ codebaseAgentRef.current.stop();
236
+ codebaseAgentRef.current.stopWatching();
237
+ setWatcherEnabled(false);
238
+ // Don't call close() - let it resume when returning
239
+ }
240
+ };
241
+ }, []); // Only run once on mount
242
+ // Export stop function for use in commands (like /home)
243
+ useEffect(() => {
244
+ // Store global reference to stop function for /home command
245
+ global.__stopCodebaseIndexing = async () => {
246
+ if (codebaseAgentRef.current) {
247
+ await codebaseAgentRef.current.stop();
248
+ setCodebaseIndexing(false);
249
+ }
250
+ };
251
+ return () => {
252
+ delete global.__stopCodebaseIndexing;
253
+ };
254
+ }, []);
98
255
  // Persist yolo mode to localStorage
99
256
  useEffect(() => {
100
257
  try {
@@ -1161,7 +1318,24 @@ export default function ChatScreen({ skipWelcome }) {
1161
1318
  ` | ${vscodeState.editorContext.activeFile}`,
1162
1319
  vscodeState.vscodeConnectionStatus === 'connected' &&
1163
1320
  vscodeState.editorContext.selectedText &&
1164
- ` | ${vscodeState.editorContext.selectedText.length} chars selected`))))),
1321
+ ` | ${vscodeState.editorContext.selectedText.length} chars selected`))),
1322
+ codebaseIndexing && codebaseProgress && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
1323
+ React.createElement(Text, { color: "cyan", dimColor: true },
1324
+ React.createElement(Spinner, { type: "dots" }),
1325
+ " Indexing codebase...",
1326
+ ' ',
1327
+ codebaseProgress.processedFiles,
1328
+ "/",
1329
+ codebaseProgress.totalFiles,
1330
+ " files",
1331
+ codebaseProgress.totalChunks > 0 &&
1332
+ ` (${codebaseProgress.totalChunks} chunks)`))),
1333
+ !codebaseIndexing && watcherEnabled && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
1334
+ React.createElement(Text, { color: "green", dimColor: true }, "\u2609 File watcher active - monitoring code changes"))),
1335
+ fileUpdateNotification && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
1336
+ React.createElement(Text, { color: "yellow", dimColor: true },
1337
+ "\u26C1 Updated: ",
1338
+ fileUpdateNotification.file))))),
1165
1339
  isCompressing && (React.createElement(Box, { marginTop: 1 },
1166
1340
  React.createElement(Text, { color: "cyan" },
1167
1341
  React.createElement(Spinner, { type: "dots" }),
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ onBack: () => void;
4
+ onSave?: () => void;
5
+ inlineMode?: boolean;
6
+ };
7
+ export default function CodeBaseConfigScreen({ onBack, onSave, inlineMode, }: Props): React.JSX.Element;
8
+ export {};
@@ -0,0 +1,323 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import Gradient from 'ink-gradient';
4
+ import { Alert } from '@inkjs/ui';
5
+ import TextInput from 'ink-text-input';
6
+ import { loadCodebaseConfig, saveCodebaseConfig, } from '../../utils/codebaseConfig.js';
7
+ const focusEventTokenRegex = /(?:\x1b)?\[[0-9;]*[IO]/g;
8
+ const isFocusEventInput = (value) => {
9
+ if (!value) {
10
+ return false;
11
+ }
12
+ if (value === '\x1b[I' ||
13
+ value === '\x1b[O' ||
14
+ value === '[I' ||
15
+ value === '[O') {
16
+ return true;
17
+ }
18
+ const trimmed = value.trim();
19
+ if (!trimmed) {
20
+ return false;
21
+ }
22
+ const tokens = trimmed.match(focusEventTokenRegex);
23
+ if (!tokens) {
24
+ return false;
25
+ }
26
+ const normalized = trimmed.replace(/\s+/g, '');
27
+ const tokensCombined = tokens.join('');
28
+ return tokensCombined === normalized;
29
+ };
30
+ const stripFocusArtifacts = (value) => {
31
+ if (!value) {
32
+ return '';
33
+ }
34
+ return value
35
+ .replace(/\x1b\[[0-9;]*[IO]/g, '')
36
+ .replace(/\[[0-9;]*[IO]/g, '')
37
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
38
+ };
39
+ export default function CodeBaseConfigScreen({ onBack, onSave, inlineMode = false, }) {
40
+ // Configuration state
41
+ const [enabled, setEnabled] = useState(false);
42
+ const [embeddingModelName, setEmbeddingModelName] = useState('');
43
+ const [embeddingBaseUrl, setEmbeddingBaseUrl] = useState('');
44
+ const [embeddingApiKey, setEmbeddingApiKey] = useState('');
45
+ const [embeddingDimensions, setEmbeddingDimensions] = useState(1536);
46
+ const [batchMaxLines, setBatchMaxLines] = useState(10);
47
+ const [batchConcurrency, setBatchConcurrency] = useState(1);
48
+ // UI state
49
+ const [currentField, setCurrentField] = useState('enabled');
50
+ const [isEditing, setIsEditing] = useState(false);
51
+ const [errors, setErrors] = useState([]);
52
+ // Scrolling configuration
53
+ const MAX_VISIBLE_FIELDS = 8;
54
+ const allFields = [
55
+ 'enabled',
56
+ 'embeddingModelName',
57
+ 'embeddingBaseUrl',
58
+ 'embeddingApiKey',
59
+ 'embeddingDimensions',
60
+ 'batchMaxLines',
61
+ 'batchConcurrency',
62
+ ];
63
+ const currentFieldIndex = allFields.indexOf(currentField);
64
+ const totalFields = allFields.length;
65
+ useEffect(() => {
66
+ loadConfiguration();
67
+ }, []);
68
+ const loadConfiguration = () => {
69
+ const config = loadCodebaseConfig();
70
+ setEnabled(config.enabled);
71
+ setEmbeddingModelName(config.embedding.modelName);
72
+ setEmbeddingBaseUrl(config.embedding.baseUrl);
73
+ setEmbeddingApiKey(config.embedding.apiKey);
74
+ setEmbeddingDimensions(config.embedding.dimensions);
75
+ setBatchMaxLines(config.batch.maxLines);
76
+ setBatchConcurrency(config.batch.concurrency);
77
+ };
78
+ const saveConfiguration = () => {
79
+ // Validation
80
+ const validationErrors = [];
81
+ if (enabled) {
82
+ // Embedding configuration is required
83
+ if (!embeddingModelName.trim()) {
84
+ validationErrors.push('Embedding model name is required when enabled');
85
+ }
86
+ if (!embeddingBaseUrl.trim()) {
87
+ validationErrors.push('Embedding base URL is required when enabled');
88
+ }
89
+ if (!embeddingApiKey.trim()) {
90
+ validationErrors.push('Embedding API key is required when enabled');
91
+ }
92
+ if (embeddingDimensions <= 0) {
93
+ validationErrors.push('Embedding dimensions must be greater than 0');
94
+ }
95
+ // Batch configuration validation
96
+ if (batchMaxLines <= 0) {
97
+ validationErrors.push('Batch max lines must be greater than 0');
98
+ }
99
+ if (batchConcurrency <= 0) {
100
+ validationErrors.push('Batch concurrency must be greater than 0');
101
+ }
102
+ // LLM is optional - no validation needed
103
+ }
104
+ if (validationErrors.length > 0) {
105
+ setErrors(validationErrors);
106
+ return;
107
+ }
108
+ try {
109
+ const config = {
110
+ enabled,
111
+ embedding: {
112
+ modelName: embeddingModelName,
113
+ baseUrl: embeddingBaseUrl,
114
+ apiKey: embeddingApiKey,
115
+ dimensions: embeddingDimensions,
116
+ },
117
+ llm: {
118
+ modelName: '',
119
+ baseUrl: '',
120
+ apiKey: '',
121
+ },
122
+ batch: {
123
+ maxLines: batchMaxLines,
124
+ concurrency: batchConcurrency,
125
+ },
126
+ };
127
+ saveCodebaseConfig(config);
128
+ setErrors([]);
129
+ // Trigger codebase config reload in ChatScreen
130
+ if (global.__reloadCodebaseConfig) {
131
+ global.__reloadCodebaseConfig();
132
+ }
133
+ onSave?.();
134
+ }
135
+ catch (error) {
136
+ setErrors([
137
+ error instanceof Error ? error.message : 'Failed to save configuration',
138
+ ]);
139
+ }
140
+ };
141
+ const renderField = (field) => {
142
+ const isActive = field === currentField;
143
+ const isCurrentlyEditing = isActive && isEditing;
144
+ switch (field) {
145
+ case 'enabled':
146
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
147
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
148
+ isActive ? '❯ ' : ' ',
149
+ "CodeBase Enabled:"),
150
+ React.createElement(Box, { marginLeft: 3 },
151
+ React.createElement(Text, { color: "gray" },
152
+ enabled ? '[✓] Enabled' : '[ ] Disabled',
153
+ " (Press Enter to toggle)"))));
154
+ case 'embeddingModelName':
155
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
156
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
157
+ isActive ? '❯ ' : ' ',
158
+ "Embedding Model Name:"),
159
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
160
+ React.createElement(Text, { color: "cyan" },
161
+ React.createElement(TextInput, { value: embeddingModelName, onChange: value => setEmbeddingModelName(stripFocusArtifacts(value)), onSubmit: () => setIsEditing(false) })))),
162
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
163
+ React.createElement(Text, { color: "gray" }, embeddingModelName || 'Not set')))));
164
+ case 'embeddingBaseUrl':
165
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
166
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
167
+ isActive ? '❯ ' : ' ',
168
+ "Embedding Base URL:"),
169
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
170
+ React.createElement(Text, { color: "cyan" },
171
+ React.createElement(TextInput, { value: embeddingBaseUrl, onChange: value => setEmbeddingBaseUrl(stripFocusArtifacts(value)), onSubmit: () => setIsEditing(false) })))),
172
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
173
+ React.createElement(Text, { color: "gray" }, embeddingBaseUrl || 'Not set')))));
174
+ case 'embeddingApiKey':
175
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
176
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
177
+ isActive ? '❯ ' : ' ',
178
+ "Embedding API Key:"),
179
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
180
+ React.createElement(Text, { color: "cyan" },
181
+ React.createElement(TextInput, { value: embeddingApiKey, onChange: value => setEmbeddingApiKey(stripFocusArtifacts(value)), onSubmit: () => setIsEditing(false), mask: "*" })))),
182
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
183
+ React.createElement(Text, { color: "gray" }, embeddingApiKey ? '••••••••' : 'Not set')))));
184
+ case 'embeddingDimensions':
185
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
186
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
187
+ isActive ? '❯ ' : ' ',
188
+ "Embedding Dimensions:"),
189
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
190
+ React.createElement(Text, { color: "cyan" },
191
+ React.createElement(TextInput, { value: embeddingDimensions.toString(), onChange: value => {
192
+ const num = parseInt(stripFocusArtifacts(value) || '0');
193
+ if (!isNaN(num)) {
194
+ setEmbeddingDimensions(num);
195
+ }
196
+ }, onSubmit: () => setIsEditing(false) })))),
197
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
198
+ React.createElement(Text, { color: "gray" }, embeddingDimensions)))));
199
+ case 'batchMaxLines':
200
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
201
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
202
+ isActive ? '❯ ' : ' ',
203
+ "Batch Max Lines:"),
204
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
205
+ React.createElement(Text, { color: "cyan" },
206
+ React.createElement(TextInput, { value: batchMaxLines.toString(), onChange: value => {
207
+ const num = parseInt(stripFocusArtifacts(value) || '0');
208
+ if (!isNaN(num)) {
209
+ setBatchMaxLines(num);
210
+ }
211
+ }, onSubmit: () => setIsEditing(false) })))),
212
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
213
+ React.createElement(Text, { color: "gray" }, batchMaxLines)))));
214
+ case 'batchConcurrency':
215
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
216
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
217
+ isActive ? '❯ ' : ' ',
218
+ "Batch Concurrency:"),
219
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
220
+ React.createElement(Text, { color: "cyan" },
221
+ React.createElement(TextInput, { value: batchConcurrency.toString(), onChange: value => {
222
+ const num = parseInt(stripFocusArtifacts(value) || '0');
223
+ if (!isNaN(num)) {
224
+ setBatchConcurrency(num);
225
+ }
226
+ }, onSubmit: () => setIsEditing(false) })))),
227
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
228
+ React.createElement(Text, { color: "gray" }, batchConcurrency)))));
229
+ default:
230
+ return null;
231
+ }
232
+ };
233
+ useInput((rawInput, key) => {
234
+ const input = stripFocusArtifacts(rawInput);
235
+ if (!input && isFocusEventInput(rawInput)) {
236
+ return;
237
+ }
238
+ if (isFocusEventInput(rawInput)) {
239
+ return;
240
+ }
241
+ // When editing, only handle submission
242
+ if (isEditing) {
243
+ // TextInput handles the actual editing
244
+ // Escape to cancel editing
245
+ if (key.escape) {
246
+ setIsEditing(false);
247
+ loadConfiguration(); // Reset to saved values
248
+ }
249
+ return;
250
+ }
251
+ // Navigation
252
+ if (key.upArrow) {
253
+ const currentIndex = allFields.indexOf(currentField);
254
+ if (currentIndex > 0) {
255
+ setCurrentField(allFields[currentIndex - 1]);
256
+ }
257
+ return;
258
+ }
259
+ if (key.downArrow) {
260
+ const currentIndex = allFields.indexOf(currentField);
261
+ if (currentIndex < allFields.length - 1) {
262
+ setCurrentField(allFields[currentIndex + 1]);
263
+ }
264
+ return;
265
+ }
266
+ // Toggle enabled field
267
+ if (key.return && currentField === 'enabled') {
268
+ setEnabled(!enabled);
269
+ return;
270
+ }
271
+ // Enter editing mode for text fields
272
+ if (key.return && currentField !== 'enabled') {
273
+ setIsEditing(true);
274
+ return;
275
+ }
276
+ // Save configuration (Ctrl+S or Escape when not editing)
277
+ if ((key.ctrl && input === 's') || key.escape) {
278
+ saveConfiguration();
279
+ if (!errors.length) {
280
+ onBack();
281
+ }
282
+ return;
283
+ }
284
+ });
285
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
286
+ !inlineMode && (React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor: 'cyan', paddingX: 2 },
287
+ React.createElement(Box, { flexDirection: "column" },
288
+ React.createElement(Gradient, { name: "rainbow" }, "CodeBase Configuration"),
289
+ React.createElement(Text, { color: "gray", dimColor: true }, "Configure codebase indexing and search settings")))),
290
+ React.createElement(Box, { marginBottom: 1 },
291
+ React.createElement(Text, { color: "yellow", bold: true },
292
+ "Settings (",
293
+ currentFieldIndex + 1,
294
+ "/",
295
+ totalFields,
296
+ ")"),
297
+ totalFields > MAX_VISIBLE_FIELDS && (React.createElement(Text, { color: "gray", dimColor: true },
298
+ ' ',
299
+ "\u00B7 \u2191\u2193 to scroll"))),
300
+ React.createElement(Box, { flexDirection: "column" }, (() => {
301
+ // Calculate visible window
302
+ if (allFields.length <= MAX_VISIBLE_FIELDS) {
303
+ // Show all fields if less than max
304
+ return allFields.map(field => renderField(field));
305
+ }
306
+ // Calculate scroll window
307
+ const halfWindow = Math.floor(MAX_VISIBLE_FIELDS / 2);
308
+ let startIndex = Math.max(0, currentFieldIndex - halfWindow);
309
+ let endIndex = Math.min(allFields.length, startIndex + MAX_VISIBLE_FIELDS);
310
+ // Adjust if we're near the end
311
+ if (endIndex - startIndex < MAX_VISIBLE_FIELDS) {
312
+ startIndex = Math.max(0, endIndex - MAX_VISIBLE_FIELDS);
313
+ }
314
+ const visibleFields = allFields.slice(startIndex, endIndex);
315
+ return visibleFields.map(field => renderField(field));
316
+ })()),
317
+ errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
318
+ React.createElement(Text, { color: "red", bold: true }, "Errors:"),
319
+ errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
320
+ "\u2022 ",
321
+ error))))),
322
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(Alert, { variant: "info" }, "Editing mode: Type to edit, Enter to save, Esc to cancel")) : (React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit/toggle, Ctrl+S or Esc to save")))));
323
+ }
@@ -62,6 +62,10 @@ const toolCategories = [
62
62
  'ace-clear_cache',
63
63
  ],
64
64
  },
65
+ {
66
+ name: 'Codebase Search Tools',
67
+ tools: ['codebase-search'],
68
+ },
65
69
  {
66
70
  name: 'Terminal Tools',
67
71
  tools: ['terminal-execute'],
@@ -10,6 +10,7 @@ import ProxyConfigScreen from './ProxyConfigScreen.js';
10
10
  import SubAgentConfigScreen from './SubAgentConfigScreen.js';
11
11
  import SubAgentListScreen from './SubAgentListScreen.js';
12
12
  import SensitiveCommandConfigScreen from './SensitiveCommandConfigScreen.js';
13
+ import CodeBaseConfigScreen from './CodeBaseConfigScreen.js';
13
14
  export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
14
15
  const [infoText, setInfoText] = useState('Start a new chat conversation');
15
16
  const [inlineView, setInlineView] = useState('menu');
@@ -35,6 +36,11 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
35
36
  value: 'proxy',
36
37
  infoText: 'Configure system proxy and browser for web search and fetch',
37
38
  },
39
+ {
40
+ label: 'CodeBase Settings',
41
+ value: 'codebase',
42
+ infoText: 'Configure codebase indexing with embedding and LLM models',
43
+ },
38
44
  {
39
45
  label: 'System Prompt Settings',
40
46
  value: 'systemprompt',
@@ -71,13 +77,16 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
71
77
  setInfoText(newInfoText);
72
78
  }, []);
73
79
  const handleInlineMenuSelect = useCallback((value) => {
74
- // Handle inline views (config, proxy, subagent) or pass through to parent
80
+ // Handle inline views (config, proxy, codebase, subagent) or pass through to parent
75
81
  if (value === 'config') {
76
82
  setInlineView('config');
77
83
  }
78
84
  else if (value === 'proxy') {
79
85
  setInlineView('proxy-config');
80
86
  }
87
+ else if (value === 'codebase') {
88
+ setInlineView('codebase-config');
89
+ }
81
90
  else if (value === 'subagent') {
82
91
  setInlineView('subagent-list');
83
92
  }
@@ -141,6 +150,8 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
141
150
  React.createElement(ConfigScreen, { onBack: handleBackToMenu, onSave: handleConfigSave, inlineMode: true }))),
142
151
  inlineView === 'proxy-config' && (React.createElement(Box, { paddingX: 1 },
143
152
  React.createElement(ProxyConfigScreen, { onBack: handleBackToMenu, onSave: handleConfigSave, inlineMode: true }))),
153
+ inlineView === 'codebase-config' && (React.createElement(Box, { paddingX: 1 },
154
+ React.createElement(CodeBaseConfigScreen, { onBack: handleBackToMenu, onSave: handleConfigSave, inlineMode: true }))),
144
155
  inlineView === 'subagent-list' && (React.createElement(Box, { paddingX: 1 },
145
156
  React.createElement(SubAgentListScreen, { onBack: handleBackToMenu, onAdd: handleSubAgentAdd, onEdit: handleSubAgentEdit, inlineMode: true }))),
146
157
  inlineView === 'subagent-add' && (React.createElement(Box, { paddingX: 1 },
@@ -0,0 +1,20 @@
1
+ export interface CodebaseConfig {
2
+ enabled: boolean;
3
+ embedding: {
4
+ modelName: string;
5
+ baseUrl: string;
6
+ apiKey: string;
7
+ dimensions: number;
8
+ };
9
+ llm: {
10
+ modelName: string;
11
+ baseUrl: string;
12
+ apiKey: string;
13
+ };
14
+ batch: {
15
+ maxLines: number;
16
+ concurrency: number;
17
+ };
18
+ }
19
+ export declare const loadCodebaseConfig: () => CodebaseConfig;
20
+ export declare const saveCodebaseConfig: (config: CodebaseConfig) => void;