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.
Files changed (53) hide show
  1. package/dist/App.d.ts.map +1 -1
  2. package/dist/App.js +13 -7
  3. package/dist/App.js.map +1 -1
  4. package/dist/agent/agent.d.ts +2 -0
  5. package/dist/agent/agent.d.ts.map +1 -1
  6. package/dist/agent/agent.js +111 -10
  7. package/dist/agent/agent.js.map +1 -1
  8. package/dist/agent/backends/anthropic.d.ts.map +1 -1
  9. package/dist/agent/backends/anthropic.js +15 -3
  10. package/dist/agent/backends/anthropic.js.map +1 -1
  11. package/dist/agent/backends/gemini.d.ts.map +1 -1
  12. package/dist/agent/backends/gemini.js +12 -0
  13. package/dist/agent/backends/gemini.js.map +1 -1
  14. package/dist/agent/backends/index.d.ts +1 -1
  15. package/dist/agent/backends/index.d.ts.map +1 -1
  16. package/dist/agent/backends/openai_compatible.d.ts.map +1 -1
  17. package/dist/agent/backends/openai_compatible.js +12 -0
  18. package/dist/agent/backends/openai_compatible.js.map +1 -1
  19. package/dist/agent/backends/types.d.ts +21 -1
  20. package/dist/agent/backends/types.d.ts.map +1 -1
  21. package/dist/agent/runtime/capabilities.d.ts +2 -1
  22. package/dist/agent/runtime/capabilities.d.ts.map +1 -1
  23. package/dist/agent/runtime/capabilities.js +1 -0
  24. package/dist/agent/runtime/capabilities.js.map +1 -1
  25. package/dist/agent/tools/index.d.ts +1 -0
  26. package/dist/agent/tools/index.d.ts.map +1 -1
  27. package/dist/agent/tools/index.js +6 -1
  28. package/dist/agent/tools/index.js.map +1 -1
  29. package/dist/agent/tools/screenshot.d.ts +23 -0
  30. package/dist/agent/tools/screenshot.d.ts.map +1 -0
  31. package/dist/agent/tools/screenshot.js +735 -0
  32. package/dist/agent/tools/screenshot.js.map +1 -0
  33. package/dist/components/InputArea.d.ts +1 -0
  34. package/dist/components/InputArea.d.ts.map +1 -1
  35. package/dist/components/InputArea.js +59 -1
  36. package/dist/components/InputArea.js.map +1 -1
  37. package/dist/utils/path_complete.d.ts +13 -0
  38. package/dist/utils/path_complete.d.ts.map +1 -0
  39. package/dist/utils/path_complete.js +162 -0
  40. package/dist/utils/path_complete.js.map +1 -0
  41. package/package.json +1 -1
  42. package/src/App.tsx +14 -7
  43. package/src/agent/agent.ts +116 -11
  44. package/src/agent/backends/anthropic.ts +15 -5
  45. package/src/agent/backends/gemini.ts +12 -0
  46. package/src/agent/backends/index.ts +1 -0
  47. package/src/agent/backends/openai_compatible.ts +12 -0
  48. package/src/agent/backends/types.ts +25 -1
  49. package/src/agent/runtime/capabilities.ts +2 -1
  50. package/src/agent/tools/index.ts +6 -1
  51. package/src/agent/tools/screenshot.ts +821 -0
  52. package/src/components/InputArea.tsx +70 -3
  53. 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
+ }