zerg-ztc 0.1.3 → 0.1.4

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 (72) hide show
  1. package/dist/App.d.ts.map +1 -1
  2. package/dist/App.js +71 -13
  3. package/dist/App.js.map +1 -1
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +3 -1
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/commands/index.d.ts.map +1 -1
  8. package/dist/agent/commands/index.js +3 -1
  9. package/dist/agent/commands/index.js.map +1 -1
  10. package/dist/agent/commands/input_mode.d.ts +3 -0
  11. package/dist/agent/commands/input_mode.d.ts.map +1 -0
  12. package/dist/agent/commands/input_mode.js +21 -0
  13. package/dist/agent/commands/input_mode.js.map +1 -0
  14. package/dist/agent/commands/keybindings.d.ts +3 -0
  15. package/dist/agent/commands/keybindings.d.ts.map +1 -0
  16. package/dist/agent/commands/keybindings.js +38 -0
  17. package/dist/agent/commands/keybindings.js.map +1 -0
  18. package/dist/agent/commands/types.d.ts +2 -0
  19. package/dist/agent/commands/types.d.ts.map +1 -1
  20. package/dist/cli.js +38 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/components/FullScreen.d.ts.map +1 -1
  23. package/dist/components/FullScreen.js +29 -29
  24. package/dist/components/FullScreen.js.map +1 -1
  25. package/dist/components/InputArea.d.ts.map +1 -1
  26. package/dist/components/InputArea.js +476 -19
  27. package/dist/components/InputArea.js.map +1 -1
  28. package/dist/components/StatusBar.d.ts +1 -0
  29. package/dist/components/StatusBar.d.ts.map +1 -1
  30. package/dist/components/StatusBar.js +2 -1
  31. package/dist/components/StatusBar.js.map +1 -1
  32. package/dist/types.d.ts +1 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/ui/core/input_segments.d.ts +1 -0
  35. package/dist/ui/core/input_segments.d.ts.map +1 -1
  36. package/dist/ui/core/input_segments.js +46 -14
  37. package/dist/ui/core/input_segments.js.map +1 -1
  38. package/dist/ui/core/types.d.ts +1 -0
  39. package/dist/ui/core/types.d.ts.map +1 -1
  40. package/dist/ui/ink/render.d.ts +3 -1
  41. package/dist/ui/ink/render.d.ts.map +1 -1
  42. package/dist/ui/ink/render.js +7 -5
  43. package/dist/ui/ink/render.js.map +1 -1
  44. package/dist/ui/views/app.d.ts +2 -1
  45. package/dist/ui/views/app.d.ts.map +1 -1
  46. package/dist/ui/views/app.js +2 -1
  47. package/dist/ui/views/app.js.map +1 -1
  48. package/dist/ui/views/header.d.ts.map +1 -1
  49. package/dist/ui/views/header.js +8 -5
  50. package/dist/ui/views/header.js.map +1 -1
  51. package/dist/ui/views/status_bar.d.ts +2 -1
  52. package/dist/ui/views/status_bar.d.ts.map +1 -1
  53. package/dist/ui/views/status_bar.js +5 -1
  54. package/dist/ui/views/status_bar.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/App.tsx +71 -13
  57. package/src/agent/agent.ts +3 -1
  58. package/src/agent/commands/index.ts +4 -0
  59. package/src/agent/commands/input_mode.ts +22 -0
  60. package/src/agent/commands/keybindings.ts +40 -0
  61. package/src/agent/commands/types.ts +2 -0
  62. package/src/cli.tsx +43 -1
  63. package/src/components/FullScreen.tsx +39 -34
  64. package/src/components/InputArea.tsx +489 -19
  65. package/src/components/StatusBar.tsx +3 -0
  66. package/src/types.ts +1 -0
  67. package/src/ui/core/input_segments.ts +49 -14
  68. package/src/ui/core/types.ts +1 -0
  69. package/src/ui/ink/render.tsx +16 -5
  70. package/src/ui/views/app.ts +3 -0
  71. package/src/ui/views/header.ts +8 -5
  72. package/src/ui/views/status_bar.ts +6 -0
@@ -36,20 +36,24 @@ export function clampCursor(segments: InputSegment[], cursor: InputCursor): Inpu
36
36
 
37
37
  export function insertText(state: InputState, text: string): InputState {
38
38
  if (text.length === 0) return state;
39
- const segments = [...state.segments];
39
+ // Deep copy segments to avoid mutating original state
40
+ const segments: InputSegment[] = state.segments.map(s => ({ ...s }));
40
41
  const cursor = clampCursor(segments, state.cursor);
41
42
 
42
43
  if (cursor.index === segments.length) {
43
44
  const last = segments[segments.length - 1];
44
45
  if (last && last.type === 'text') {
45
- last.text += text;
46
+ const newText = last.text + text;
47
+ segments[segments.length - 1] = { ...last, text: newText };
46
48
  } else {
47
49
  segments.push({ type: 'text', text });
48
50
  }
51
+ const finalSegment = segments[segments.length - 1];
52
+ const finalOffset = finalSegment.type === 'text' ? finalSegment.text.length : 1;
49
53
  return {
50
54
  ...state,
51
55
  segments: normalizeSegments(segments),
52
- cursor: { index: segments.length - 1, offset: (segments[segments.length - 1] as any).text.length }
56
+ cursor: { index: segments.length - 1, offset: finalOffset }
53
57
  };
54
58
  }
55
59
 
@@ -57,7 +61,8 @@ export function insertText(state: InputState, text: string): InputState {
57
61
  if (segment.type === 'text') {
58
62
  const before = segment.text.slice(0, cursor.offset);
59
63
  const after = segment.text.slice(cursor.offset);
60
- segment.text = before + text + after;
64
+ const newText = before + text + after;
65
+ segments[cursor.index] = { ...segment, text: newText };
61
66
  return {
62
67
  ...state,
63
68
  segments: normalizeSegments(segments),
@@ -94,18 +99,20 @@ export function insertBadge(state: InputState, segment: InputSegment): InputStat
94
99
  }
95
100
 
96
101
  export function backspace(state: InputState): InputState {
97
- const segments = [...state.segments];
102
+ // Deep copy segments to avoid mutating original state
103
+ const segments: InputSegment[] = state.segments.map(s => ({ ...s }));
98
104
  const cursor = clampCursor(segments, state.cursor);
99
105
 
100
106
  if (cursor.index === segments.length) {
101
107
  if (segments.length === 0) return state;
102
108
  const last = segments[segments.length - 1];
103
109
  if (last.type === 'text' && last.text.length > 0) {
104
- last.text = last.text.slice(0, -1);
110
+ const newText = last.text.slice(0, -1);
111
+ segments[segments.length - 1] = { ...last, text: newText };
105
112
  return {
106
113
  ...state,
107
114
  segments: normalizeSegments(segments),
108
- cursor: { index: segments.length - 1, offset: last.text.length }
115
+ cursor: { index: segments.length - 1, offset: newText.length }
109
116
  };
110
117
  }
111
118
  segments.pop();
@@ -119,7 +126,8 @@ export function backspace(state: InputState): InputState {
119
126
  const segment = segments[cursor.index];
120
127
  if (segment.type === 'text') {
121
128
  if (cursor.offset > 0) {
122
- segment.text = segment.text.slice(0, cursor.offset - 1) + segment.text.slice(cursor.offset);
129
+ const newText = segment.text.slice(0, cursor.offset - 1) + segment.text.slice(cursor.offset);
130
+ segments[cursor.index] = { ...segment, text: newText };
123
131
  return {
124
132
  ...state,
125
133
  segments: normalizeSegments(segments),
@@ -130,7 +138,7 @@ export function backspace(state: InputState): InputState {
130
138
  const prev = segments[cursor.index - 1];
131
139
  if (prev.type === 'text') {
132
140
  const prevLen = prev.text.length;
133
- prev.text += segment.text;
141
+ segments[cursor.index - 1] = { ...prev, text: prev.text + segment.text };
134
142
  segments.splice(cursor.index, 1);
135
143
  return {
136
144
  ...state,
@@ -159,7 +167,7 @@ export function backspace(state: InputState): InputState {
159
167
  const prev = segments[cursor.index - 1];
160
168
  if (prev.type === 'text') {
161
169
  const prevLen = prev.text.length;
162
- prev.text = prev.text.slice(0, -1);
170
+ segments[cursor.index - 1] = { ...prev, text: prev.text.slice(0, -1) };
163
171
  return {
164
172
  ...state,
165
173
  segments: normalizeSegments(segments),
@@ -175,7 +183,8 @@ export function backspace(state: InputState): InputState {
175
183
  }
176
184
 
177
185
  export function deleteForward(state: InputState): InputState {
178
- const segments = [...state.segments];
186
+ // Deep copy segments to avoid mutating original state
187
+ const segments: InputSegment[] = state.segments.map(s => ({ ...s }));
179
188
  const cursor = clampCursor(segments, state.cursor);
180
189
 
181
190
  if (cursor.index === segments.length) return state;
@@ -183,7 +192,8 @@ export function deleteForward(state: InputState): InputState {
183
192
  const segment = segments[cursor.index];
184
193
  if (segment.type === 'text') {
185
194
  if (cursor.offset < segment.text.length) {
186
- segment.text = segment.text.slice(0, cursor.offset) + segment.text.slice(cursor.offset + 1);
195
+ const newText = segment.text.slice(0, cursor.offset) + segment.text.slice(cursor.offset + 1);
196
+ segments[cursor.index] = { ...segment, text: newText };
187
197
  return {
188
198
  ...state,
189
199
  segments: normalizeSegments(segments),
@@ -193,7 +203,7 @@ export function deleteForward(state: InputState): InputState {
193
203
  if (cursor.index + 1 >= segments.length) return state;
194
204
  const next = segments[cursor.index + 1];
195
205
  if (next.type === 'text') {
196
- segment.text += next.text;
206
+ segments[cursor.index] = { ...segment, text: segment.text + next.text };
197
207
  segments.splice(cursor.index + 1, 1);
198
208
  return {
199
209
  ...state,
@@ -221,7 +231,7 @@ export function deleteForward(state: InputState): InputState {
221
231
  if (cursor.index + 1 >= segments.length) return state;
222
232
  const next = segments[cursor.index + 1];
223
233
  if (next.type === 'text') {
224
- next.text = next.text.slice(1);
234
+ segments[cursor.index + 1] = { ...next, text: next.text.slice(1) };
225
235
  return {
226
236
  ...state,
227
237
  segments: normalizeSegments(segments),
@@ -236,6 +246,31 @@ export function deleteForward(state: InputState): InputState {
236
246
  };
237
247
  }
238
248
 
249
+ export function killToEnd(state: InputState): InputState {
250
+ const segments = [...state.segments];
251
+ const cursor = clampCursor(segments, state.cursor);
252
+
253
+ if (cursor.index >= segments.length) return state;
254
+
255
+ const segment = segments[cursor.index];
256
+ if (segment.type === 'text') {
257
+ segment.text = segment.text.slice(0, cursor.offset);
258
+ segments.splice(cursor.index + 1);
259
+ return {
260
+ ...state,
261
+ segments: normalizeSegments(segments),
262
+ cursor: { index: cursor.index, offset: segment.text.length }
263
+ };
264
+ }
265
+
266
+ segments.splice(cursor.index);
267
+ return {
268
+ ...state,
269
+ segments: normalizeSegments(segments),
270
+ cursor: { index: Math.min(cursor.index, segments.length), offset: 0 }
271
+ };
272
+ }
273
+
239
274
  export function moveLeft(state: InputState): InputState {
240
275
  const segments = state.segments;
241
276
  const cursor = clampCursor(segments, state.cursor);
@@ -24,6 +24,7 @@ export interface Style {
24
24
  justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between';
25
25
  borderStyle?: string;
26
26
  borderColor?: string;
27
+ overflow?: 'visible' | 'hidden';
27
28
  }
28
29
 
29
30
  export interface TextStyle {
@@ -1,8 +1,8 @@
1
- import React from 'react';
1
+ import React, { memo } from 'react';
2
2
  import { Box, Text, type BoxProps } from 'ink';
3
3
  import { LayoutNode, TextNode } from '../core/types.js';
4
4
 
5
- function renderText(node: TextNode): React.ReactElement {
5
+ const InkText = memo(function InkText({ node }: { node: TextNode }): React.ReactElement {
6
6
  const style = node.style || {};
7
7
  return (
8
8
  <Text
@@ -15,11 +15,11 @@ function renderText(node: TextNode): React.ReactElement {
15
15
  {node.text}
16
16
  </Text>
17
17
  );
18
- }
18
+ });
19
19
 
20
- export function InkNode({ node }: { node: LayoutNode }): React.ReactElement {
20
+ function InkNodeInner({ node }: { node: LayoutNode }): React.ReactElement {
21
21
  if (node.type === 'text') {
22
- return renderText(node);
22
+ return <InkText node={node} />;
23
23
  }
24
24
 
25
25
  const style = node.style || {};
@@ -33,13 +33,22 @@ export function InkNode({ node }: { node: LayoutNode }): React.ReactElement {
33
33
  padding={style.padding}
34
34
  paddingX={style.paddingX}
35
35
  paddingY={style.paddingY}
36
+ paddingLeft={style.paddingLeft}
37
+ paddingRight={style.paddingRight}
38
+ paddingTop={style.paddingTop}
39
+ paddingBottom={style.paddingBottom}
36
40
  margin={style.margin}
37
41
  marginX={style.marginX}
38
42
  marginY={style.marginY}
43
+ marginLeft={style.marginLeft}
44
+ marginRight={style.marginRight}
45
+ marginTop={style.marginTop}
46
+ marginBottom={style.marginBottom}
39
47
  alignItems={style.alignItems}
40
48
  justifyContent={style.justifyContent}
41
49
  borderStyle={style.borderStyle as BoxProps['borderStyle']}
42
50
  borderColor={style.borderColor}
51
+ overflow={style.overflow}
43
52
  >
44
53
  {node.children.map((child, index) => (
45
54
  <InkNode key={`${child.type}-${index}`} node={child} />
@@ -47,3 +56,5 @@ export function InkNode({ node }: { node: LayoutNode }): React.ReactElement {
47
56
  </Box>
48
57
  );
49
58
  }
59
+
60
+ export const InkNode = memo(InkNodeInner);
@@ -20,6 +20,7 @@ interface AppViewProps {
20
20
  provider?: string;
21
21
  model?: string;
22
22
  emulationId?: string;
23
+ inputMode?: 'queue' | 'interrupt';
23
24
  toast?: string | null;
24
25
  debug?: boolean;
25
26
  expandToolOutputs?: boolean;
@@ -52,6 +53,7 @@ export function buildAppView({
52
53
  provider,
53
54
  model,
54
55
  emulationId,
56
+ inputMode,
55
57
  toast,
56
58
  debug = false,
57
59
  expandToolOutputs = false
@@ -84,6 +86,7 @@ export function buildAppView({
84
86
  provider,
85
87
  model,
86
88
  emulationId,
89
+ inputMode,
87
90
  toast,
88
91
  debug
89
92
  }),
@@ -15,13 +15,13 @@ export function buildHeaderView({
15
15
  showHelp = true,
16
16
  debug = false
17
17
  }: HeaderProps): LayoutNode {
18
+ // Single-row header to minimize vertical space
18
19
  return box([
19
20
  box([
20
21
  text(title, { bold: true }),
22
+ text(' ', {}),
21
23
  text('ZTC', { color: 'gray', dimColor: true })
22
- ], {
23
- flexDirection: 'column'
24
- }),
24
+ ], { flexDirection: 'row' }),
25
25
  showHelp
26
26
  ? box([
27
27
  dateLabel ? text(dateLabel, { color: 'gray', dimColor: true }) : text('', {}),
@@ -34,9 +34,12 @@ export function buildHeaderView({
34
34
  ], { flexDirection: 'row' })
35
35
  : box([], { flexDirection: 'row' })
36
36
  ], {
37
- flexDirection: 'column',
37
+ flexDirection: 'row',
38
38
  justifyContent: 'space-between',
39
39
  paddingX: 1,
40
- paddingY: 1
40
+ height: 1,
41
+ flexShrink: 0,
42
+ borderStyle: debug ? 'single' : undefined,
43
+ borderColor: debug ? 'cyan' : undefined
41
44
  });
42
45
  }
@@ -11,6 +11,7 @@ interface StatusBarProps {
11
11
  provider?: string;
12
12
  model?: string;
13
13
  emulationId?: string;
14
+ inputMode?: 'queue' | 'interrupt';
14
15
  toast?: string | null;
15
16
  debug?: boolean;
16
17
  }
@@ -36,6 +37,7 @@ export function buildStatusBarView({
36
37
  provider,
37
38
  model,
38
39
  emulationId,
40
+ inputMode,
39
41
  toast,
40
42
  debug = false
41
43
  }: StatusBarProps): LayoutNode {
@@ -78,6 +80,8 @@ export function buildStatusBarView({
78
80
  (provider || model) ? text(' • ', { color: 'gray', dimColor: true }) : text('', {}),
79
81
  emulationId ? text(`emu:${emulationId}`, { color: 'gray', dimColor: true }) : text('', {}),
80
82
  emulationId ? text(' • ', { color: 'gray', dimColor: true }) : text('', {}),
83
+ inputMode ? text(`mode:${inputMode}`, { color: 'gray', dimColor: true }) : text('', {}),
84
+ inputMode ? text(' • ', { color: 'gray', dimColor: true }) : text('', {}),
81
85
  state.tokensUsed !== undefined && state.tokensUsed > 0
82
86
  ? text(`${state.tokensUsed.toLocaleString()} tok`, { color: 'gray', dimColor: true })
83
87
  : text('', {}),
@@ -98,6 +102,8 @@ export function buildStatusBarView({
98
102
  flexDirection: 'row',
99
103
  justifyContent: 'space-between',
100
104
  paddingX: 1,
105
+ height: 1,
106
+ flexShrink: 0,
101
107
  borderStyle: debug ? 'single' : undefined,
102
108
  borderColor: debug ? 'gray' : undefined
103
109
  });