zerg-ztc 0.1.0 → 0.1.2

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 (42) hide show
  1. package/dist/App.d.ts.map +1 -1
  2. package/dist/App.js +16 -0
  3. package/dist/App.js.map +1 -1
  4. package/dist/cli.js +13 -8
  5. package/dist/cli.js.map +1 -1
  6. package/dist/components/InputArea.d.ts.map +1 -1
  7. package/dist/components/InputArea.js +9 -1
  8. package/dist/components/InputArea.js.map +1 -1
  9. package/dist/ui/views/input_area.js +1 -1
  10. package/dist/ui/web/frame_render.d.ts +23 -0
  11. package/dist/ui/web/frame_render.d.ts.map +1 -0
  12. package/dist/ui/web/frame_render.js +73 -0
  13. package/dist/ui/web/frame_render.js.map +1 -0
  14. package/dist/ui/web/index.d.ts +2 -0
  15. package/dist/ui/web/index.d.ts.map +1 -0
  16. package/dist/ui/web/index.js +2 -0
  17. package/dist/ui/web/index.js.map +1 -0
  18. package/dist/ui/web/render.d.ts +6 -0
  19. package/dist/ui/web/render.d.ts.map +1 -0
  20. package/dist/ui/web/render.js +30 -0
  21. package/dist/ui/web/render.js.map +1 -0
  22. package/dist/web/mirror_hook.d.ts +4 -0
  23. package/dist/web/mirror_hook.d.ts.map +1 -0
  24. package/dist/web/mirror_hook.js +22 -0
  25. package/dist/web/mirror_hook.js.map +1 -0
  26. package/dist/web/mirror_server.d.ts +16 -0
  27. package/dist/web/mirror_server.d.ts.map +1 -0
  28. package/dist/web/mirror_server.js +177 -0
  29. package/dist/web/mirror_server.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/App.tsx +18 -0
  32. package/src/cli.tsx +15 -8
  33. package/src/components/InputArea.tsx +9 -1
  34. package/src/ui/views/input_area.ts +1 -1
  35. package/src/ui/web/frame_render.tsx +148 -0
  36. package/src/ui/web/index.tsx +1 -0
  37. package/src/ui/web/render.tsx +41 -0
  38. package/src/web/index.html +352 -0
  39. package/src/web/mirror-favicon.svg +4 -0
  40. package/src/web/mirror.html +641 -0
  41. package/src/web/mirror_hook.ts +25 -0
  42. package/src/web/mirror_server.ts +204 -0
@@ -0,0 +1,177 @@
1
+ import { createServer } from 'http';
2
+ import { readFile, mkdir, writeFile } from 'fs/promises';
3
+ import { resolve, join, basename } from 'path';
4
+ import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ import { computeLayout } from '../ui/core/index.js';
7
+ export class MirrorServer {
8
+ clients = new Map();
9
+ port;
10
+ server = createServer(this.handleRequest.bind(this));
11
+ lastTree = null;
12
+ inputBus;
13
+ constructor(port, inputBus) {
14
+ this.port = port;
15
+ this.inputBus = inputBus;
16
+ }
17
+ start() {
18
+ this.server.listen(this.port);
19
+ }
20
+ stop() {
21
+ this.server.close();
22
+ }
23
+ publish(tree) {
24
+ this.lastTree = tree;
25
+ for (const client of this.clients.values()) {
26
+ this.sendLayout(client, tree);
27
+ }
28
+ }
29
+ async handleRequest(req, res) {
30
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
31
+ if (url.pathname === '/') {
32
+ const htmlPath = new URL('./mirror.html', import.meta.url);
33
+ const html = await readFile(htmlPath, 'utf-8').catch(async () => {
34
+ const fallback = resolve(process.cwd(), 'src/web/mirror.html');
35
+ return readFile(fallback, 'utf-8');
36
+ });
37
+ res.writeHead(200, { 'Content-Type': 'text/html' });
38
+ res.end(html);
39
+ return;
40
+ }
41
+ if (url.pathname === '/mirror-favicon.svg' || url.pathname === '/favicon.svg' || url.pathname === '/favicon.ico') {
42
+ const iconPath = new URL('./mirror-favicon.svg', import.meta.url);
43
+ const icon = await readFile(iconPath, 'utf-8').catch(async () => {
44
+ const fallback = resolve(process.cwd(), 'src/web/mirror-favicon.svg');
45
+ return readFile(fallback, 'utf-8');
46
+ });
47
+ const contentType = url.pathname === '/favicon.ico' ? 'image/x-icon' : 'image/svg+xml';
48
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
49
+ res.end(icon);
50
+ return;
51
+ }
52
+ if (url.pathname === '/events') {
53
+ const id = url.searchParams.get('id') || `client_${Date.now()}`;
54
+ res.writeHead(200, {
55
+ 'Content-Type': 'text/event-stream',
56
+ 'Cache-Control': 'no-cache',
57
+ Connection: 'keep-alive'
58
+ });
59
+ res.write('\n');
60
+ const client = {
61
+ id,
62
+ res,
63
+ cols: 80,
64
+ rows: 24
65
+ };
66
+ this.clients.set(id, client);
67
+ if (this.lastTree) {
68
+ this.sendLayout(client, this.lastTree);
69
+ }
70
+ req.on('close', () => {
71
+ this.clients.delete(id);
72
+ });
73
+ return;
74
+ }
75
+ if (url.pathname === '/size' && req.method === 'POST') {
76
+ const id = url.searchParams.get('id');
77
+ if (!id || !this.clients.has(id)) {
78
+ res.writeHead(400);
79
+ res.end();
80
+ return;
81
+ }
82
+ const body = await new Promise(resolve => {
83
+ let data = '';
84
+ req.on('data', chunk => { data += chunk; });
85
+ req.on('end', () => resolve(data));
86
+ });
87
+ try {
88
+ const parsed = JSON.parse(body);
89
+ const client = this.clients.get(id);
90
+ if (client) {
91
+ client.cols = parsed.cols;
92
+ client.rows = parsed.rows;
93
+ if (this.lastTree) {
94
+ this.sendLayout(client, this.lastTree);
95
+ }
96
+ }
97
+ }
98
+ catch {
99
+ // Ignore invalid payloads
100
+ }
101
+ res.writeHead(204);
102
+ res.end();
103
+ return;
104
+ }
105
+ if (url.pathname === '/input' && req.method === 'POST') {
106
+ const body = await new Promise(resolve => {
107
+ let data = '';
108
+ req.on('data', chunk => { data += chunk; });
109
+ req.on('end', () => resolve(data));
110
+ });
111
+ try {
112
+ const parsed = JSON.parse(body);
113
+ this.inputBus?.emit(parsed);
114
+ }
115
+ catch {
116
+ // Ignore invalid payloads
117
+ }
118
+ res.writeHead(204);
119
+ res.end();
120
+ return;
121
+ }
122
+ if (url.pathname === '/upload' && req.method === 'POST') {
123
+ const buffer = await new Promise(resolve => {
124
+ const chunks = [];
125
+ req.on('data', chunk => { chunks.push(Buffer.from(chunk)); });
126
+ req.on('end', () => resolve(Buffer.concat(chunks)));
127
+ });
128
+ const headerName = req.headers['x-filename'];
129
+ const rawName = typeof headerName === 'string' && headerName.trim().length > 0 ? headerName : 'upload.bin';
130
+ const safeName = basename(rawName).replace(/[^a-zA-Z0-9._-]/g, '_');
131
+ const dir = join(homedir(), '.ztc', 'uploads');
132
+ await mkdir(dir, { recursive: true });
133
+ const filePath = join(dir, `${Date.now()}-${randomUUID().slice(0, 8)}-${safeName}`);
134
+ await writeFile(filePath, buffer);
135
+ res.writeHead(200, { 'Content-Type': 'application/json' });
136
+ res.end(JSON.stringify({ path: filePath }));
137
+ return;
138
+ }
139
+ if (url.pathname === '/file' && req.method === 'GET') {
140
+ const rawPath = url.searchParams.get('path');
141
+ if (!rawPath) {
142
+ res.writeHead(400);
143
+ res.end();
144
+ return;
145
+ }
146
+ const allowedRoots = [
147
+ join(homedir(), '.ztc', 'uploads'),
148
+ join(homedir(), '.ztc', 'clipboard')
149
+ ];
150
+ const resolvedPath = resolve(rawPath);
151
+ const allowed = allowedRoots.some(root => resolvedPath.startsWith(resolve(root)));
152
+ if (!allowed) {
153
+ res.writeHead(403);
154
+ res.end();
155
+ return;
156
+ }
157
+ try {
158
+ const data = await readFile(resolvedPath);
159
+ res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
160
+ res.end(data);
161
+ }
162
+ catch {
163
+ res.writeHead(404);
164
+ res.end();
165
+ }
166
+ return;
167
+ }
168
+ res.writeHead(404);
169
+ res.end();
170
+ }
171
+ sendLayout(client, tree) {
172
+ const frame = computeLayout(tree, client.cols, client.rows);
173
+ const payload = JSON.stringify(frame);
174
+ client.res.write(`data: ${payload}\n\n`);
175
+ }
176
+ }
177
+ //# sourceMappingURL=mirror_server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mirror_server.js","sourceRoot":"","sources":["../../src/web/mirror_server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerg-ztc",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Zerg Terminal Client - CLI agent for continual AI systems",
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.tsx CHANGED
@@ -73,6 +73,24 @@ export const App: React.FC = () => {
73
73
  const [agent, setAgent] = useState<Agent | null>(createAgent);
74
74
  const [expandToolOutputs, setExpandToolOutputs] = useState(false);
75
75
  const [skills, setSkills] = useState<Skill[]>([]);
76
+
77
+ React.useEffect(() => {
78
+ let active = true;
79
+ configStore.load(true).then(() => {
80
+ if (!active) return;
81
+ setMessages(prev => {
82
+ if (
83
+ prev.length === 1 &&
84
+ prev[0]?.role === 'system' &&
85
+ prev[0].content.startsWith('Welcome to ZTC')
86
+ ) {
87
+ return [getWelcomeMessage()];
88
+ }
89
+ return prev;
90
+ });
91
+ }).catch(() => {});
92
+ return () => { active = false; };
93
+ }, []);
76
94
  const [inputSnapshot, setInputSnapshot] = useState<InputState>({
77
95
  segments: [],
78
96
  cursor: { index: 0, offset: 0 },
package/src/cli.tsx CHANGED
@@ -4,6 +4,7 @@ import { render } from 'ink';
4
4
  import { resolve } from 'path';
5
5
  import { statSync } from 'fs';
6
6
  import { App } from './App.js';
7
+ import { configStore } from './config.js';
7
8
 
8
9
  // --- CLI Entry Point ---
9
10
 
@@ -69,12 +70,18 @@ if (cwdIndex >= 0) {
69
70
  }
70
71
  }
71
72
 
72
- // Render the app
73
- // Note: FullScreen component handles alternate screen buffer
74
- const { waitUntilExit } = render(<App />);
73
+ async function main(): Promise<void> {
74
+ await configStore.load(true);
75
75
 
76
- // Handle clean exit
77
- waitUntilExit().then(() => {
78
- console.log('Goodbye from ZTC! 👋');
79
- process.exit(0);
80
- });
76
+ // Render the app
77
+ // Note: FullScreen component handles alternate screen buffer
78
+ const { waitUntilExit } = render(<App />);
79
+
80
+ // Handle clean exit
81
+ waitUntilExit().then(() => {
82
+ console.log('Goodbye from ZTC! 👋');
83
+ process.exit(0);
84
+ });
85
+ }
86
+
87
+ void main();
@@ -213,7 +213,7 @@ export const InputArea: React.FC<InputAreaProps> = ({
213
213
  if (disabled) {
214
214
  lines.push(prompt + chalk.gray(placeholder));
215
215
  } else if (overlayState.segments.length === 0) {
216
- lines.push(prompt + chalk.inverse(' ') + chalk.gray(placeholder));
216
+ lines.push(prompt + chalk.inverse('|') + chalk.gray(placeholder));
217
217
  } else {
218
218
  wrapped.lines.forEach((lineTokens, index) => {
219
219
  const prefix = index === 0 ? prompt : ' ';
@@ -297,6 +297,10 @@ export const InputArea: React.FC<InputAreaProps> = ({
297
297
 
298
298
  const handleInput = useCallback((input: string, key: InputKey) => {
299
299
  if (disabled) return;
300
+ const backspaceFallback = input === '\b' || input === '\x7f';
301
+ if (backspaceFallback && !key.backspace && !key.delete) {
302
+ key.backspace = true;
303
+ }
300
304
 
301
305
  if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
302
306
  void handleClipboardImagePaste('state');
@@ -382,6 +386,10 @@ export const InputArea: React.FC<InputAreaProps> = ({
382
386
 
383
387
  const handleOverlayInput = useCallback((input: string, key: InputKey) => {
384
388
  if (disabled) return;
389
+ const backspaceFallback = input === '\b' || input === '\x7f';
390
+ if (backspaceFallback && !key.backspace && !key.delete) {
391
+ key.backspace = true;
392
+ }
385
393
  const current = overlayStateRef.current;
386
394
 
387
395
  if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
@@ -185,7 +185,7 @@ export function buildInputAreaView({
185
185
  box([
186
186
  text(promptPrefix, { color: promptColor, bold: true }),
187
187
  box([
188
- text(' ', { inverse: true }),
188
+ text('|', { inverse: true }),
189
189
  text(placeholder, { color: 'gray', dimColor: true })
190
190
  ], { flexDirection: 'row' })
191
191
  ], { flexDirection: 'row' })
@@ -0,0 +1,148 @@
1
+ import React, { useMemo } from 'react';
2
+ import { LayoutNode, LayoutFrame, computeLayout } from '../core/index.js';
3
+
4
+ type BadgeInfo = {
5
+ type: 'paste' | 'file' | 'image';
6
+ preview: string;
7
+ full: string;
8
+ path?: string;
9
+ };
10
+
11
+ interface FrameRendererProps {
12
+ node: LayoutNode;
13
+ cols: number;
14
+ rows: number;
15
+ cellWidth: number;
16
+ cellHeight: number;
17
+ debugBorders?: boolean;
18
+ onBadgeHover?: (badge: BadgeInfo, x: number, y: number) => void;
19
+ onBadgeLeave?: () => void;
20
+ onBadgeClick?: (badge: BadgeInfo, x: number, y: number) => void;
21
+ onBadgeDblClick?: (badge: BadgeInfo, x: number, y: number) => void;
22
+ }
23
+
24
+ function colorMap(color?: string): string {
25
+ const map: Record<string, string> = {
26
+ gray: '#9a9a9a',
27
+ magenta: '#c084fc',
28
+ yellow: '#facc15',
29
+ green: '#34d399',
30
+ red: '#f87171',
31
+ cyan: '#67e8f9',
32
+ blue: '#60a5fa',
33
+ white: '#e5e7eb'
34
+ };
35
+ return map[color || ''] || color || '#e6e6e6';
36
+ }
37
+
38
+ function renderFrame(
39
+ frame: LayoutFrame,
40
+ keyPrefix: string,
41
+ cellWidth: number,
42
+ cellHeight: number,
43
+ debugBorders: boolean,
44
+ onBadgeHover?: (badge: BadgeInfo, x: number, y: number) => void,
45
+ onBadgeLeave?: () => void,
46
+ onBadgeClick?: (badge: BadgeInfo, x: number, y: number) => void,
47
+ onBadgeDblClick?: (badge: BadgeInfo, x: number, y: number) => void,
48
+ offsetX = 0,
49
+ offsetY = 0,
50
+ out: React.ReactElement[] = []
51
+ ): React.ReactElement[] {
52
+ const x = offsetX + frame.x;
53
+ const y = offsetY + frame.y;
54
+ const keyBase = `${keyPrefix}-${x}-${y}`;
55
+
56
+ if (frame.node.type === 'text') {
57
+ const style = frame.node.style || {};
58
+ if (debugBorders) {
59
+ out.push(
60
+ <div
61
+ key={`${keyBase}-debug`}
62
+ style={{
63
+ position: 'absolute',
64
+ left: x * cellWidth,
65
+ top: y * cellHeight,
66
+ width: frame.width * cellWidth,
67
+ height: frame.height * cellHeight,
68
+ borderStyle: 'dashed',
69
+ borderWidth: 1,
70
+ borderColor: '#3a3a3a',
71
+ pointerEvents: 'none'
72
+ }}
73
+ />
74
+ );
75
+ }
76
+ const badge = style.badge;
77
+ out.push(
78
+ <span
79
+ key={`${keyBase}-text`}
80
+ style={{
81
+ position: 'absolute',
82
+ left: x * cellWidth,
83
+ top: y * cellHeight,
84
+ color: style.inverse ? '#111' : colorMap(style.color),
85
+ fontWeight: style.bold ? 600 : undefined,
86
+ opacity: style.dimColor ? 0.6 : undefined,
87
+ background: style.inverse ? '#e6e6e6' : undefined,
88
+ colorScheme: 'only light',
89
+ cursor: badge ? 'pointer' : undefined
90
+ }}
91
+ title={badge?.preview || undefined}
92
+ onMouseEnter={badge ? (evt) => onBadgeHover?.(badge, evt.clientX, evt.clientY) : undefined}
93
+ onMouseLeave={badge ? () => onBadgeLeave?.() : undefined}
94
+ onClick={badge ? (evt) => onBadgeClick?.(badge, evt.clientX, evt.clientY) : undefined}
95
+ onDoubleClick={badge ? (evt) => onBadgeDblClick?.(badge, evt.clientX, evt.clientY) : undefined}
96
+ >
97
+ {frame.node.text}
98
+ </span>
99
+ );
100
+ return out;
101
+ }
102
+
103
+ if (frame.node.type === 'box' && (frame.node.style?.borderStyle || debugBorders)) {
104
+ out.push(
105
+ <div
106
+ key={`${keyBase}-border`}
107
+ style={{
108
+ position: 'absolute',
109
+ left: x * cellWidth,
110
+ top: y * cellHeight,
111
+ width: frame.width * cellWidth,
112
+ height: frame.height * cellHeight,
113
+ borderStyle: frame.node.style?.borderStyle ? 'solid' : 'dashed',
114
+ borderWidth: 1,
115
+ borderColor: colorMap(frame.node.style?.borderColor || '#3a3a3a'),
116
+ pointerEvents: 'none'
117
+ }}
118
+ />
119
+ );
120
+ }
121
+
122
+ frame.children.forEach((child, idx) => {
123
+ renderFrame(child, `${keyBase}-${idx}`, cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick, x, y, out);
124
+ });
125
+
126
+ return out;
127
+ }
128
+
129
+ export const WebFrameRenderer: React.FC<FrameRendererProps> = ({
130
+ node,
131
+ cols,
132
+ rows,
133
+ cellWidth,
134
+ cellHeight,
135
+ debugBorders = false,
136
+ onBadgeHover,
137
+ onBadgeLeave,
138
+ onBadgeClick,
139
+ onBadgeDblClick
140
+ }) => {
141
+ const frame = useMemo(() => computeLayout(node, cols, rows), [node, cols, rows]);
142
+ const elements = useMemo(
143
+ () => renderFrame(frame, 'root', cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick),
144
+ [frame, cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick]
145
+ );
146
+
147
+ return <>{elements}</>;
148
+ };
@@ -0,0 +1 @@
1
+ export { WebNode } from './render.js';
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { LayoutNode, TextNode } from '../core/types.js';
3
+ import { toCssStyle } from '../core/style.js';
4
+
5
+ const monoStyle: React.CSSProperties = {
6
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
7
+ fontSize: 14,
8
+ lineHeight: '16px',
9
+ whiteSpace: 'pre'
10
+ };
11
+
12
+ function renderText(node: TextNode): React.ReactElement {
13
+ const style: React.CSSProperties = {
14
+ ...monoStyle,
15
+ color: node.style?.color,
16
+ fontWeight: node.style?.bold ? 600 : undefined,
17
+ opacity: node.style?.dimColor ? 0.6 : undefined,
18
+ background: node.style?.inverse ? '#111' : undefined,
19
+ colorScheme: 'only light'
20
+ };
21
+ return <span style={style}>{node.text}</span>;
22
+ }
23
+
24
+ export function WebNode({ node }: { node: LayoutNode }): React.ReactElement {
25
+ if (node.type === 'text') {
26
+ return renderText(node);
27
+ }
28
+
29
+ const style = {
30
+ ...monoStyle,
31
+ ...toCssStyle(node.style)
32
+ } as React.CSSProperties;
33
+
34
+ return (
35
+ <div style={style}>
36
+ {node.children.map((child, index) => (
37
+ <WebNode key={`${child.type}-${index}`} node={child} />
38
+ ))}
39
+ </div>
40
+ );
41
+ }