zerg-ztc 0.1.6 → 0.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/dist/App.d.ts.map +1 -1
- package/dist/App.js +13 -7
- package/dist/App.js.map +1 -1
- package/dist/agent/agent.d.ts +2 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +111 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/backends/anthropic.d.ts.map +1 -1
- package/dist/agent/backends/anthropic.js +15 -3
- package/dist/agent/backends/anthropic.js.map +1 -1
- package/dist/agent/backends/gemini.d.ts.map +1 -1
- package/dist/agent/backends/gemini.js +12 -0
- package/dist/agent/backends/gemini.js.map +1 -1
- package/dist/agent/backends/index.d.ts +1 -1
- package/dist/agent/backends/index.d.ts.map +1 -1
- package/dist/agent/backends/openai_compatible.d.ts.map +1 -1
- package/dist/agent/backends/openai_compatible.js +12 -0
- package/dist/agent/backends/openai_compatible.js.map +1 -1
- package/dist/agent/backends/types.d.ts +21 -1
- package/dist/agent/backends/types.d.ts.map +1 -1
- package/dist/agent/runtime/capabilities.d.ts +2 -1
- package/dist/agent/runtime/capabilities.d.ts.map +1 -1
- package/dist/agent/runtime/capabilities.js +1 -0
- package/dist/agent/runtime/capabilities.js.map +1 -1
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.d.ts.map +1 -1
- package/dist/agent/tools/index.js +6 -1
- package/dist/agent/tools/index.js.map +1 -1
- package/dist/agent/tools/screenshot.d.ts +23 -0
- package/dist/agent/tools/screenshot.d.ts.map +1 -0
- package/dist/agent/tools/screenshot.js +735 -0
- package/dist/agent/tools/screenshot.js.map +1 -0
- package/dist/components/InputArea.d.ts +1 -0
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +59 -1
- package/dist/components/InputArea.js.map +1 -1
- package/dist/utils/path_complete.d.ts +13 -0
- package/dist/utils/path_complete.d.ts.map +1 -0
- package/dist/utils/path_complete.js +162 -0
- package/dist/utils/path_complete.js.map +1 -0
- package/package.json +1 -1
- package/src/App.tsx +14 -7
- package/src/agent/agent.ts +116 -11
- package/src/agent/backends/anthropic.ts +15 -5
- package/src/agent/backends/gemini.ts +12 -0
- package/src/agent/backends/index.ts +1 -0
- package/src/agent/backends/openai_compatible.ts +12 -0
- package/src/agent/backends/types.ts +25 -1
- package/src/agent/runtime/capabilities.ts +2 -1
- package/src/agent/tools/index.ts +6 -1
- package/src/agent/tools/screenshot.ts +821 -0
- package/src/components/InputArea.tsx +70 -3
- package/src/utils/path_complete.ts +173 -0
|
@@ -8,6 +8,7 @@ import { debugLog } from '../debug/logger.js';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { saveClipboardImage } from '../utils/clipboard_image.js';
|
|
10
10
|
import { renderImagePreview } from '../utils/image_preview.js';
|
|
11
|
+
import { completePath } from '../utils/path_complete.js';
|
|
11
12
|
import {
|
|
12
13
|
createEmptyState,
|
|
13
14
|
insertText,
|
|
@@ -40,6 +41,7 @@ interface InputAreaProps {
|
|
|
40
41
|
placeholder?: string;
|
|
41
42
|
historyEnabled?: boolean;
|
|
42
43
|
debug?: boolean;
|
|
44
|
+
cwd?: string; // Working directory for path tab completion
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export type { InputState } from '../ui/core/input_state.js';
|
|
@@ -100,8 +102,8 @@ function reducer(state: InputState, action: InputAction): InputState {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
export const InputArea: React.FC<InputAreaProps> = ({
|
|
104
|
-
onSubmit,
|
|
105
|
+
export const InputArea: React.FC<InputAreaProps> = ({
|
|
106
|
+
onSubmit,
|
|
105
107
|
onCommand,
|
|
106
108
|
commands = [],
|
|
107
109
|
onStateChange,
|
|
@@ -111,7 +113,8 @@ export const InputArea: React.FC<InputAreaProps> = ({
|
|
|
111
113
|
disabled = false,
|
|
112
114
|
placeholder = 'Type a message...',
|
|
113
115
|
historyEnabled = true,
|
|
114
|
-
debug = false
|
|
116
|
+
debug = false,
|
|
117
|
+
cwd = process.cwd()
|
|
115
118
|
}) => {
|
|
116
119
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
117
120
|
const stateRef = React.useRef(state);
|
|
@@ -279,6 +282,60 @@ export const InputArea: React.FC<InputAreaProps> = ({
|
|
|
279
282
|
return { ...current, segments, cursor: { index: cursor.index, offset: replacedLeft.length }, historyIdx: -1 };
|
|
280
283
|
}, []);
|
|
281
284
|
|
|
285
|
+
// Tab completion state
|
|
286
|
+
const tabCompletionAlternativesRef = React.useRef<string[]>([]);
|
|
287
|
+
const lastTabPrefixRef = React.useRef<string>('');
|
|
288
|
+
|
|
289
|
+
// Handle tab completion for shell commands
|
|
290
|
+
const handleTabComplete = useCallback(async (current: InputState): Promise<InputState> => {
|
|
291
|
+
const plainText = getPlainText(current.segments);
|
|
292
|
+
|
|
293
|
+
// Only complete if this looks like a shell command with a path
|
|
294
|
+
// Match: !cd path, !ls path, !cat path, etc.
|
|
295
|
+
const shellMatch = plainText.match(/^!(\w+)\s+(.*)$/);
|
|
296
|
+
if (!shellMatch) {
|
|
297
|
+
// Also support just !cd without a path yet
|
|
298
|
+
if (plainText.match(/^!cd\s*$/)) {
|
|
299
|
+
// Show home directory
|
|
300
|
+
const result = await completePath('~/', cwd);
|
|
301
|
+
if (result && result.alternatives.length > 0) {
|
|
302
|
+
tabCompletionAlternativesRef.current = result.alternatives;
|
|
303
|
+
onToast?.(`Completions: ${result.alternatives.slice(0, 5).join(', ')}${result.alternatives.length > 5 ? '...' : ''}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return current;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const partialPath = shellMatch[2];
|
|
310
|
+
const commandPrefix = `!${shellMatch[1]} `;
|
|
311
|
+
|
|
312
|
+
// Try to complete the path
|
|
313
|
+
const result = await completePath(partialPath, cwd);
|
|
314
|
+
if (!result) {
|
|
315
|
+
onToast?.('No completions');
|
|
316
|
+
return current;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If we have alternatives and this is a repeat tab, show them
|
|
320
|
+
if (result.alternatives.length > 1) {
|
|
321
|
+
tabCompletionAlternativesRef.current = result.alternatives;
|
|
322
|
+
lastTabPrefixRef.current = partialPath;
|
|
323
|
+
onToast?.(`Completions: ${result.alternatives.slice(0, 6).join(' ')}${result.alternatives.length > 6 ? ' ...' : ''}`);
|
|
324
|
+
} else {
|
|
325
|
+
tabCompletionAlternativesRef.current = [];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Update the input with the completed path
|
|
329
|
+
const newText = commandPrefix + result.completed;
|
|
330
|
+
const newSegments: InputSegment[] = [{ type: 'text', text: newText }];
|
|
331
|
+
return {
|
|
332
|
+
...current,
|
|
333
|
+
segments: newSegments,
|
|
334
|
+
cursor: { index: 0, offset: newText.length },
|
|
335
|
+
historyIdx: -1
|
|
336
|
+
};
|
|
337
|
+
}, [cwd, onToast]);
|
|
338
|
+
|
|
282
339
|
const inputRenderCount = React.useRef(0);
|
|
283
340
|
React.useEffect(() => {
|
|
284
341
|
inputRenderCount.current += 1;
|
|
@@ -599,6 +656,16 @@ export const InputArea: React.FC<InputAreaProps> = ({
|
|
|
599
656
|
return;
|
|
600
657
|
}
|
|
601
658
|
|
|
659
|
+
// Tab completion for shell commands
|
|
660
|
+
if (safeKey.tab || input === '\t') {
|
|
661
|
+
void handleTabComplete(state).then(nextState => {
|
|
662
|
+
if (nextState !== state) {
|
|
663
|
+
dispatch({ type: 'apply', state: nextState });
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
602
669
|
if (key.leftArrow) {
|
|
603
670
|
if (key.ctrl) {
|
|
604
671
|
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { resolve, dirname, basename } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get path completions for tab autocomplete
|
|
7
|
+
*/
|
|
8
|
+
export async function getPathCompletions(
|
|
9
|
+
partial: string,
|
|
10
|
+
cwd: string
|
|
11
|
+
): Promise<string[]> {
|
|
12
|
+
// Expand tilde
|
|
13
|
+
let expandedPartial = partial;
|
|
14
|
+
if (partial.startsWith('~/')) {
|
|
15
|
+
expandedPartial = resolve(homedir(), partial.slice(2));
|
|
16
|
+
} else if (partial === '~') {
|
|
17
|
+
expandedPartial = homedir();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolve the path relative to cwd
|
|
21
|
+
const fullPath = resolve(cwd, expandedPartial);
|
|
22
|
+
|
|
23
|
+
// Determine the directory to list and the prefix to match
|
|
24
|
+
let dirToList: string;
|
|
25
|
+
let prefix: string;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { stat } = await import('fs/promises');
|
|
29
|
+
const stats = await stat(fullPath);
|
|
30
|
+
if (stats.isDirectory()) {
|
|
31
|
+
// If it's a directory, list its contents
|
|
32
|
+
dirToList = fullPath;
|
|
33
|
+
prefix = '';
|
|
34
|
+
} else {
|
|
35
|
+
// It's a file, no completions
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Path doesn't exist, so complete the last component
|
|
40
|
+
dirToList = dirname(fullPath);
|
|
41
|
+
prefix = basename(fullPath).toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const entries = await readdir(dirToList, { withFileTypes: true });
|
|
46
|
+
const matches = entries
|
|
47
|
+
.filter(entry => entry.name.toLowerCase().startsWith(prefix))
|
|
48
|
+
.map(entry => {
|
|
49
|
+
const name = entry.name;
|
|
50
|
+
const suffix = entry.isDirectory() ? '/' : '';
|
|
51
|
+
return name + suffix;
|
|
52
|
+
})
|
|
53
|
+
.sort();
|
|
54
|
+
|
|
55
|
+
return matches;
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Complete a partial path, returning the completed path or null if no unique completion
|
|
63
|
+
*/
|
|
64
|
+
export async function completePath(
|
|
65
|
+
partial: string,
|
|
66
|
+
cwd: string
|
|
67
|
+
): Promise<{ completed: string; isDirectory: boolean; alternatives: string[] } | null> {
|
|
68
|
+
// Handle empty partial - list cwd contents
|
|
69
|
+
if (!partial || partial.trim() === '') {
|
|
70
|
+
try {
|
|
71
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
72
|
+
const matches = entries.filter(e => !e.name.startsWith('.')).sort();
|
|
73
|
+
if (matches.length === 0) return null;
|
|
74
|
+
if (matches.length === 1) {
|
|
75
|
+
const entry = matches[0];
|
|
76
|
+
const suffix = entry.isDirectory() ? '/' : '';
|
|
77
|
+
return { completed: entry.name + suffix, isDirectory: entry.isDirectory(), alternatives: [] };
|
|
78
|
+
}
|
|
79
|
+
const alternatives = matches.map(e => e.name + (e.isDirectory() ? '/' : ''));
|
|
80
|
+
return { completed: '', isDirectory: false, alternatives };
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Expand tilde for internal processing
|
|
87
|
+
let expandedPartial = partial;
|
|
88
|
+
let tildePrefix = '';
|
|
89
|
+
if (partial.startsWith('~/')) {
|
|
90
|
+
expandedPartial = resolve(homedir(), partial.slice(2));
|
|
91
|
+
tildePrefix = '~/';
|
|
92
|
+
} else if (partial === '~') {
|
|
93
|
+
return { completed: '~/', isDirectory: true, alternatives: [] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fullPath = resolve(cwd, expandedPartial);
|
|
97
|
+
let dirToList: string;
|
|
98
|
+
let prefix: string;
|
|
99
|
+
let basePath: string;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const { stat } = await import('fs/promises');
|
|
103
|
+
const stats = await stat(fullPath);
|
|
104
|
+
if (stats.isDirectory()) {
|
|
105
|
+
// Already a complete directory, append / if not present
|
|
106
|
+
if (!partial.endsWith('/')) {
|
|
107
|
+
return { completed: partial + '/', isDirectory: true, alternatives: [] };
|
|
108
|
+
}
|
|
109
|
+
// List directory contents for alternatives
|
|
110
|
+
dirToList = fullPath;
|
|
111
|
+
prefix = '';
|
|
112
|
+
basePath = partial;
|
|
113
|
+
} else {
|
|
114
|
+
// Complete file path
|
|
115
|
+
return { completed: partial, isDirectory: false, alternatives: [] };
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Path doesn't exist, complete the last component
|
|
119
|
+
dirToList = dirname(fullPath);
|
|
120
|
+
prefix = basename(fullPath).toLowerCase();
|
|
121
|
+
|
|
122
|
+
// Calculate basePath - the directory portion to preserve
|
|
123
|
+
if (tildePrefix) {
|
|
124
|
+
const afterTilde = expandedPartial.slice(homedir().length + 1);
|
|
125
|
+
const dirPart = dirname(afterTilde);
|
|
126
|
+
basePath = dirPart === '.' ? tildePrefix : tildePrefix + dirPart + '/';
|
|
127
|
+
} else {
|
|
128
|
+
const dirPart = dirname(partial);
|
|
129
|
+
// Don't add ./ prefix for simple filenames
|
|
130
|
+
basePath = (dirPart === '.' || dirPart === '') ? '' : dirPart + '/';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const entries = await readdir(dirToList, { withFileTypes: true });
|
|
136
|
+
const matches = entries
|
|
137
|
+
.filter(entry => entry.name.toLowerCase().startsWith(prefix))
|
|
138
|
+
.sort();
|
|
139
|
+
|
|
140
|
+
if (matches.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (matches.length === 1) {
|
|
145
|
+
const entry = matches[0];
|
|
146
|
+
const suffix = entry.isDirectory() ? '/' : '';
|
|
147
|
+
const completed = basePath + entry.name + suffix;
|
|
148
|
+
return { completed, isDirectory: entry.isDirectory(), alternatives: [] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Find common prefix among all matches
|
|
152
|
+
const names = matches.map(e => e.name);
|
|
153
|
+
let commonPrefix = names[0];
|
|
154
|
+
for (const name of names.slice(1)) {
|
|
155
|
+
let i = 0;
|
|
156
|
+
while (i < commonPrefix.length && i < name.length && commonPrefix[i] === name[i]) {
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
commonPrefix = commonPrefix.slice(0, i);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const alternatives = matches.map(e => e.name + (e.isDirectory() ? '/' : ''));
|
|
163
|
+
const completed = basePath + commonPrefix;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
completed: completed.length > partial.length ? completed : partial,
|
|
167
|
+
isDirectory: false,
|
|
168
|
+
alternatives
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|