zerg-ztc 0.1.3 → 0.1.5
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 +183 -19
- package/dist/App.js.map +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +3 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/commands/config.d.ts.map +1 -1
- package/dist/agent/commands/config.js +68 -2
- package/dist/agent/commands/config.js.map +1 -1
- package/dist/agent/commands/index.d.ts.map +1 -1
- package/dist/agent/commands/index.js +4 -1
- package/dist/agent/commands/index.js.map +1 -1
- package/dist/agent/commands/input_mode.d.ts +3 -0
- package/dist/agent/commands/input_mode.d.ts.map +1 -0
- package/dist/agent/commands/input_mode.js +21 -0
- package/dist/agent/commands/input_mode.js.map +1 -0
- package/dist/agent/commands/keybindings.d.ts +3 -0
- package/dist/agent/commands/keybindings.d.ts.map +1 -0
- package/dist/agent/commands/keybindings.js +38 -0
- package/dist/agent/commands/keybindings.js.map +1 -0
- package/dist/agent/commands/types.d.ts +2 -0
- package/dist/agent/commands/types.d.ts.map +1 -1
- package/dist/agent/commands/update.d.ts +3 -0
- package/dist/agent/commands/update.d.ts.map +1 -0
- package/dist/agent/commands/update.js +33 -0
- package/dist/agent/commands/update.js.map +1 -0
- package/dist/cli.js +68 -16
- package/dist/cli.js.map +1 -1
- package/dist/components/ActivityLine.d.ts +11 -0
- package/dist/components/ActivityLine.d.ts.map +1 -0
- package/dist/components/ActivityLine.js +9 -0
- package/dist/components/ActivityLine.js.map +1 -0
- package/dist/components/FullScreen.d.ts +1 -0
- package/dist/components/FullScreen.d.ts.map +1 -1
- package/dist/components/FullScreen.js +30 -30
- package/dist/components/FullScreen.js.map +1 -1
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +476 -19
- package/dist/components/InputArea.js.map +1 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +41 -2
- package/dist/components/MessageList.js.map +1 -1
- package/dist/components/SingleMessage.d.ts +9 -0
- package/dist/components/SingleMessage.d.ts.map +1 -0
- package/dist/components/SingleMessage.js +27 -0
- package/dist/components/SingleMessage.js.map +1 -0
- package/dist/components/StatusBar.d.ts +2 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +3 -1
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/core/input_segments.d.ts +1 -0
- package/dist/ui/core/input_segments.d.ts.map +1 -1
- package/dist/ui/core/input_segments.js +46 -14
- package/dist/ui/core/input_segments.js.map +1 -1
- package/dist/ui/core/types.d.ts +1 -0
- package/dist/ui/core/types.d.ts.map +1 -1
- package/dist/ui/ink/render.d.ts +3 -1
- package/dist/ui/ink/render.d.ts.map +1 -1
- package/dist/ui/ink/render.js +7 -5
- package/dist/ui/ink/render.js.map +1 -1
- package/dist/ui/views/activity_line.d.ts +11 -0
- package/dist/ui/views/activity_line.d.ts.map +1 -0
- package/dist/ui/views/activity_line.js +20 -0
- package/dist/ui/views/activity_line.js.map +1 -0
- package/dist/ui/views/app.d.ts +5 -1
- package/dist/ui/views/app.d.ts.map +1 -1
- package/dist/ui/views/app.js +18 -14
- package/dist/ui/views/app.js.map +1 -1
- package/dist/ui/views/header.d.ts.map +1 -1
- package/dist/ui/views/header.js +7 -5
- package/dist/ui/views/header.js.map +1 -1
- package/dist/ui/views/input_area.d.ts.map +1 -1
- package/dist/ui/views/input_area.js +25 -12
- package/dist/ui/views/input_area.js.map +1 -1
- package/dist/ui/views/message_list.d.ts +3 -2
- package/dist/ui/views/message_list.d.ts.map +1 -1
- package/dist/ui/views/message_list.js +33 -19
- package/dist/ui/views/message_list.js.map +1 -1
- package/dist/ui/views/status_bar.d.ts +3 -1
- package/dist/ui/views/status_bar.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +8 -2
- package/dist/ui/views/status_bar.js.map +1 -1
- package/dist/utils/spinner_frames.d.ts +2 -0
- package/dist/utils/spinner_frames.d.ts.map +1 -0
- package/dist/utils/spinner_frames.js +2 -0
- package/dist/utils/spinner_frames.js.map +1 -0
- package/dist/utils/spinner_verbs.d.ts +4 -0
- package/dist/utils/spinner_verbs.d.ts.map +1 -0
- package/dist/utils/spinner_verbs.js +22 -0
- package/dist/utils/spinner_verbs.js.map +1 -0
- package/dist/utils/tool_trace.d.ts.map +1 -1
- package/dist/utils/tool_trace.js +12 -2
- package/dist/utils/tool_trace.js.map +1 -1
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +37 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +16 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +1 -1
- package/src/App.tsx +226 -32
- package/src/agent/agent.ts +3 -1
- package/src/agent/commands/config.ts +76 -2
- package/src/agent/commands/index.ts +6 -0
- package/src/agent/commands/input_mode.ts +22 -0
- package/src/agent/commands/keybindings.ts +40 -0
- package/src/agent/commands/types.ts +2 -0
- package/src/agent/commands/update.ts +32 -0
- package/src/cli.tsx +77 -15
- package/src/components/ActivityLine.tsx +23 -0
- package/src/components/FullScreen.tsx +41 -35
- package/src/components/InputArea.tsx +489 -19
- package/src/components/MessageList.tsx +52 -6
- package/src/components/SingleMessage.tsx +59 -0
- package/src/components/StatusBar.tsx +6 -0
- package/src/components/index.tsx +3 -1
- package/src/config/types.ts +1 -0
- package/src/config.ts +8 -0
- package/src/types.ts +1 -0
- package/src/ui/core/input_segments.ts +49 -14
- package/src/ui/core/types.ts +1 -0
- package/src/ui/ink/render.tsx +16 -5
- package/src/ui/views/activity_line.ts +33 -0
- package/src/ui/views/app.ts +25 -13
- package/src/ui/views/header.ts +7 -5
- package/src/ui/views/input_area.ts +28 -17
- package/src/ui/views/message_list.ts +36 -20
- package/src/ui/views/status_bar.ts +11 -1
- package/src/utils/spinner_frames.ts +1 -0
- package/src/utils/spinner_verbs.ts +23 -0
- package/src/utils/tool_trace.ts +12 -2
- package/src/utils/update.ts +44 -0
- package/src/utils/version.ts +15 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Message } from '../types.js';
|
|
4
|
+
|
|
5
|
+
interface SingleMessageProps {
|
|
6
|
+
message: Message;
|
|
7
|
+
expandToolOutputs?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RoleConfig {
|
|
11
|
+
icon: string;
|
|
12
|
+
color: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const roleConfigs: Record<string, RoleConfig> = {
|
|
17
|
+
user: { icon: '❯', color: 'blue', label: 'You' },
|
|
18
|
+
assistant: { icon: '◆', color: 'magenta', label: 'Zerg' },
|
|
19
|
+
tool: { icon: '⏺', color: 'yellow', label: 'Trace' },
|
|
20
|
+
system: { icon: '●', color: 'gray', label: 'System' }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function formatTime(date: Date): string {
|
|
24
|
+
return date.toLocaleTimeString('en-US', {
|
|
25
|
+
hour: '2-digit',
|
|
26
|
+
minute: '2-digit',
|
|
27
|
+
hour12: false
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SingleMessage: React.FC<SingleMessageProps> = ({
|
|
32
|
+
message,
|
|
33
|
+
expandToolOutputs = false
|
|
34
|
+
}) => {
|
|
35
|
+
const config = roleConfigs[message.role] || roleConfigs.system;
|
|
36
|
+
const toolOutput = message.metadata && (message.metadata as Record<string, any>).toolOutput;
|
|
37
|
+
const content = toolOutput && expandToolOutputs && toolOutput.full
|
|
38
|
+
? toolOutput.full
|
|
39
|
+
: toolOutput && toolOutput.preview
|
|
40
|
+
? toolOutput.preview
|
|
41
|
+
: message.content;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Box flexDirection="column" marginY={1} paddingX={1}>
|
|
45
|
+
<Box flexDirection="row">
|
|
46
|
+
<Text color={config.color} bold>{config.icon} {config.label}</Text>
|
|
47
|
+
<Text color="gray" dimColor> • {formatTime(message.timestamp)}</Text>
|
|
48
|
+
{message.isStreaming && <Text color="yellow" bold> ▌</Text>}
|
|
49
|
+
</Box>
|
|
50
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
51
|
+
{content.split('\n').map((line: string, i: number) => (
|
|
52
|
+
<Text key={i}>{line}</Text>
|
|
53
|
+
))}
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default SingleMessage;
|
|
@@ -13,7 +13,9 @@ interface StatusBarProps {
|
|
|
13
13
|
provider?: string;
|
|
14
14
|
model?: string;
|
|
15
15
|
emulationId?: string;
|
|
16
|
+
inputMode?: 'queue' | 'interrupt';
|
|
16
17
|
toast?: string | null;
|
|
18
|
+
spinnerLabel?: string | null;
|
|
17
19
|
debug?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -27,7 +29,9 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
27
29
|
provider,
|
|
28
30
|
model,
|
|
29
31
|
emulationId,
|
|
32
|
+
inputMode,
|
|
30
33
|
toast,
|
|
34
|
+
spinnerLabel,
|
|
31
35
|
debug = false
|
|
32
36
|
}) => {
|
|
33
37
|
const node = buildStatusBarView({
|
|
@@ -40,7 +44,9 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
40
44
|
provider,
|
|
41
45
|
model,
|
|
42
46
|
emulationId,
|
|
47
|
+
inputMode,
|
|
43
48
|
toast,
|
|
49
|
+
spinnerLabel,
|
|
44
50
|
debug
|
|
45
51
|
});
|
|
46
52
|
return <InkNode node={node} />;
|
package/src/components/index.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Component exports
|
|
2
2
|
export { Header } from './Header.js';
|
|
3
3
|
export { MessageList } from './MessageList.js';
|
|
4
|
+
export { SingleMessage } from './SingleMessage.js';
|
|
4
5
|
export { InputArea } from './InputArea.js';
|
|
5
6
|
export { StatusBar } from './StatusBar.js';
|
|
6
|
-
export { FullScreen, useScreenSize } from './FullScreen.js';
|
|
7
|
+
export { FullScreen, useScreenSize } from './FullScreen.js';
|
|
8
|
+
export { ActivityLine } from './ActivityLine.js';
|
package/src/config/types.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
4
|
|
|
5
5
|
import { ZTCConfig } from './config/types.js';
|
|
6
|
+
import { DEFAULT_SPINNER_VERBS } from './utils/spinner_verbs.js';
|
|
6
7
|
|
|
7
8
|
// --- Configuration Store ---
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ const DEFAULT_CONFIG: ZTCConfig = {
|
|
|
15
16
|
maxTokens: 4096,
|
|
16
17
|
zergEndpoint: undefined,
|
|
17
18
|
emulationId: undefined,
|
|
19
|
+
spinnerVerbs: DEFAULT_SPINNER_VERBS,
|
|
18
20
|
toolPermissions: {
|
|
19
21
|
file_read: true,
|
|
20
22
|
file_write: true,
|
|
@@ -46,6 +48,9 @@ class ConfigStore {
|
|
|
46
48
|
if (this.config.model === 'claude-opus-4-5-20250514') {
|
|
47
49
|
this.config.model = 'claude-opus-4-20250514';
|
|
48
50
|
}
|
|
51
|
+
if (!this.config.spinnerVerbs) {
|
|
52
|
+
this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
|
|
53
|
+
}
|
|
49
54
|
if (!this.config.toolPermissions) {
|
|
50
55
|
this.config.toolPermissions = { ...DEFAULT_CONFIG.toolPermissions };
|
|
51
56
|
}
|
|
@@ -92,6 +97,9 @@ class ConfigStore {
|
|
|
92
97
|
if (this.config.model === 'claude-opus-4-5-20250514') {
|
|
93
98
|
this.config.model = 'claude-opus-4-20250514';
|
|
94
99
|
}
|
|
100
|
+
if (!this.config.spinnerVerbs) {
|
|
101
|
+
this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
|
|
102
|
+
}
|
|
95
103
|
if (!this.config.toolPermissions) {
|
|
96
104
|
this.config.toolPermissions = { ...DEFAULT_CONFIG.toolPermissions };
|
|
97
105
|
}
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type MessageRole = 'user' | 'assistant' | 'tool' | 'system';
|
|
|
7
7
|
export type ToolStatus = 'pending' | 'running' | 'complete' | 'error';
|
|
8
8
|
|
|
9
9
|
export type AgentStatus = 'idle' | 'thinking' | 'tool_use' | 'streaming' | 'error';
|
|
10
|
+
export type InputMode = 'queue' | 'interrupt';
|
|
10
11
|
|
|
11
12
|
export interface ToolCall {
|
|
12
13
|
id: string;
|
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/ui/core/types.ts
CHANGED
package/src/ui/ink/render.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
20
|
+
function InkNodeInner({ node }: { node: LayoutNode }): React.ReactElement {
|
|
21
21
|
if (node.type === 'text') {
|
|
22
|
-
return
|
|
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);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AgentState } from '../../types.js';
|
|
2
|
+
import { box, text, LayoutNode } from '../core/index.js';
|
|
3
|
+
|
|
4
|
+
interface ActivityLineProps {
|
|
5
|
+
state: AgentState;
|
|
6
|
+
spinnerLabel?: string | null;
|
|
7
|
+
spinnerFrame?: string | null;
|
|
8
|
+
inputMode?: 'queue' | 'interrupt';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildActivityLineView({
|
|
12
|
+
state,
|
|
13
|
+
spinnerLabel,
|
|
14
|
+
spinnerFrame,
|
|
15
|
+
inputMode
|
|
16
|
+
}: ActivityLineProps): LayoutNode {
|
|
17
|
+
const active = state.status === 'thinking' || state.status === 'streaming';
|
|
18
|
+
if (!active || !spinnerLabel) {
|
|
19
|
+
return box([], { height: 0 });
|
|
20
|
+
}
|
|
21
|
+
const frame = spinnerFrame || '✽';
|
|
22
|
+
const hint = inputMode === 'interrupt' ? ' (esc to interrupt)' : '';
|
|
23
|
+
return box([
|
|
24
|
+
text(`${frame} ${spinnerLabel}…${hint}`, { color: 'yellow' })
|
|
25
|
+
], {
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
paddingX: 2,
|
|
28
|
+
paddingY: 0,
|
|
29
|
+
marginTop: 1,
|
|
30
|
+
marginBottom: 1,
|
|
31
|
+
height: 1
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/ui/views/app.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { buildHeaderView } from './header.js';
|
|
|
5
5
|
import { buildMessageListView } from './message_list.js';
|
|
6
6
|
import { buildInputAreaView, estimateInputLines } from './input_area.js';
|
|
7
7
|
import { buildStatusBarView } from './status_bar.js';
|
|
8
|
+
import { buildActivityLineView } from './activity_line.js';
|
|
8
9
|
|
|
9
10
|
interface AppViewProps {
|
|
10
11
|
messages: Message[];
|
|
@@ -15,12 +16,16 @@ interface AppViewProps {
|
|
|
15
16
|
rows: number;
|
|
16
17
|
commands: Array<{ name: string; description: string; usage?: string }>;
|
|
17
18
|
hasApiKey: boolean;
|
|
19
|
+
version?: string;
|
|
18
20
|
contextLength?: number;
|
|
19
21
|
contextEstimated?: boolean;
|
|
20
22
|
provider?: string;
|
|
21
23
|
model?: string;
|
|
22
24
|
emulationId?: string;
|
|
23
25
|
toast?: string | null;
|
|
26
|
+
spinnerLabel?: string | null;
|
|
27
|
+
spinnerFrame?: string | null;
|
|
28
|
+
inputMode?: 'queue' | 'interrupt';
|
|
24
29
|
debug?: boolean;
|
|
25
30
|
expandToolOutputs?: boolean;
|
|
26
31
|
}
|
|
@@ -47,23 +52,28 @@ export function buildAppView({
|
|
|
47
52
|
rows,
|
|
48
53
|
commands,
|
|
49
54
|
hasApiKey,
|
|
55
|
+
version = '0.1.0',
|
|
50
56
|
contextLength,
|
|
51
57
|
contextEstimated,
|
|
52
58
|
provider,
|
|
53
59
|
model,
|
|
54
60
|
emulationId,
|
|
61
|
+
inputMode,
|
|
55
62
|
toast,
|
|
63
|
+
spinnerLabel,
|
|
64
|
+
spinnerFrame,
|
|
56
65
|
debug = false,
|
|
57
66
|
expandToolOutputs = false
|
|
58
67
|
}: AppViewProps): LayoutNode {
|
|
59
68
|
const suggestionLines = 4;
|
|
60
|
-
const headerHeight =
|
|
69
|
+
const headerHeight = 1;
|
|
61
70
|
const statusHeight = 1;
|
|
71
|
+
const activityHeight = (agentState.status === 'thinking' || agentState.status === 'streaming') && spinnerLabel ? 3 : 0;
|
|
62
72
|
const inputLineCount = estimateInputLines(inputState.segments, cols);
|
|
63
73
|
const previewLines = estimateBadgePreviewLines(inputState);
|
|
64
|
-
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + suggestionLines + 5));
|
|
74
|
+
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + activityHeight + suggestionLines + 5));
|
|
65
75
|
const inputHeight = Math.min(Math.max(1, inputLineCount + previewLines), maxInputLines) + suggestionLines;
|
|
66
|
-
const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight), 5);
|
|
76
|
+
const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight + activityHeight), 5);
|
|
67
77
|
|
|
68
78
|
const placeholder = !hasApiKey ? 'Set API key with /config key <key>' :
|
|
69
79
|
agentState.status === 'thinking' ? 'Thinking...' :
|
|
@@ -72,28 +82,30 @@ export function buildAppView({
|
|
|
72
82
|
'Type a message or /help for commands...';
|
|
73
83
|
|
|
74
84
|
return box([
|
|
75
|
-
buildHeaderView({ version
|
|
85
|
+
buildHeaderView({ version, debug }),
|
|
76
86
|
buildMessageListView({ messages, height: contentHeight, debug, expandToolOutputs }),
|
|
87
|
+
buildActivityLineView({ state: agentState, spinnerLabel, spinnerFrame, inputMode }),
|
|
88
|
+
buildInputAreaView({
|
|
89
|
+
state: inputState,
|
|
90
|
+
placeholder,
|
|
91
|
+
disabled: agentState.status !== 'idle' && agentState.status !== 'error',
|
|
92
|
+
commands,
|
|
93
|
+
cols,
|
|
94
|
+
debug
|
|
95
|
+
}),
|
|
77
96
|
buildStatusBarView({
|
|
78
97
|
state: agentState,
|
|
79
98
|
sessionId,
|
|
80
|
-
version
|
|
99
|
+
version,
|
|
81
100
|
connectionStatus: hasApiKey ? 'connected' : 'disconnected',
|
|
82
101
|
contextLength,
|
|
83
102
|
contextEstimated,
|
|
84
103
|
provider,
|
|
85
104
|
model,
|
|
86
105
|
emulationId,
|
|
106
|
+
inputMode,
|
|
87
107
|
toast,
|
|
88
108
|
debug
|
|
89
|
-
}),
|
|
90
|
-
buildInputAreaView({
|
|
91
|
-
state: inputState,
|
|
92
|
-
placeholder,
|
|
93
|
-
disabled: agentState.status !== 'idle' && agentState.status !== 'error',
|
|
94
|
-
commands,
|
|
95
|
-
cols,
|
|
96
|
-
debug
|
|
97
109
|
})
|
|
98
110
|
], { flexDirection: 'column' });
|
|
99
111
|
}
|
package/src/ui/views/header.ts
CHANGED
|
@@ -18,10 +18,9 @@ export function buildHeaderView({
|
|
|
18
18
|
return box([
|
|
19
19
|
box([
|
|
20
20
|
text(title, { bold: true }),
|
|
21
|
+
text(' ', {}),
|
|
21
22
|
text('ZTC', { color: 'gray', dimColor: true })
|
|
22
|
-
], {
|
|
23
|
-
flexDirection: 'column'
|
|
24
|
-
}),
|
|
23
|
+
], { flexDirection: 'row' }),
|
|
25
24
|
showHelp
|
|
26
25
|
? box([
|
|
27
26
|
dateLabel ? text(dateLabel, { color: 'gray', dimColor: true }) : text('', {}),
|
|
@@ -34,9 +33,12 @@ export function buildHeaderView({
|
|
|
34
33
|
], { flexDirection: 'row' })
|
|
35
34
|
: box([], { flexDirection: 'row' })
|
|
36
35
|
], {
|
|
37
|
-
flexDirection: '
|
|
36
|
+
flexDirection: 'row',
|
|
38
37
|
justifyContent: 'space-between',
|
|
39
38
|
paddingX: 1,
|
|
40
|
-
|
|
39
|
+
flexShrink: 0,
|
|
40
|
+
borderStyle: 'single',
|
|
41
|
+
borderColor: debug ? 'cyan' : 'gray',
|
|
42
|
+
marginBottom: 1
|
|
41
43
|
});
|
|
42
44
|
}
|
|
@@ -216,19 +216,20 @@ export function buildInputAreaView({
|
|
|
216
216
|
borderColor: debug ? (disabled ? 'gray' : 'blue') : undefined
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
219
|
+
// Only render suggestion lines if there are actual command matches
|
|
220
|
+
const suggestions = commandMatches.length > 0
|
|
221
|
+
? box(
|
|
222
|
+
commandMatches.map((cmd) => {
|
|
223
|
+
const usage = cmd.usage ? ` ${truncate(cmd.usage, 36)}` : '';
|
|
224
|
+
return box([
|
|
225
|
+
text(`/${cmd.name}`, { color: 'cyan', bold: true }),
|
|
226
|
+
text(usage, { color: 'white' }),
|
|
227
|
+
text(` — ${truncate(cmd.description, 48)}`, { color: 'gray', dimColor: true })
|
|
228
|
+
], { flexDirection: 'row' });
|
|
229
|
+
}),
|
|
230
|
+
{ flexDirection: 'column', paddingX: 2 }
|
|
231
|
+
)
|
|
232
|
+
: null;
|
|
232
233
|
|
|
233
234
|
const previewLines = showBadgePreview && badgePreview && badgePreview.length > 0
|
|
234
235
|
? box(badgePreview.map(line => text(line, { color: 'gray', dimColor: true })), {
|
|
@@ -237,8 +238,18 @@ export function buildInputAreaView({
|
|
|
237
238
|
})
|
|
238
239
|
: null;
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
// Horizontal separator lines
|
|
242
|
+
const separatorTop = text('─'.repeat(Math.max(10, cols - 2)), { color: 'gray', dimColor: true });
|
|
243
|
+
const separatorBottom = text('─'.repeat(Math.max(10, cols - 2)), { color: 'gray', dimColor: true });
|
|
244
|
+
|
|
245
|
+
// Build the content array, filtering out nulls
|
|
246
|
+
const content: LayoutNode[] = [separatorTop, inputLine];
|
|
247
|
+
if (previewLines) content.push(previewLines);
|
|
248
|
+
if (suggestions) content.push(suggestions);
|
|
249
|
+
content.push(separatorBottom);
|
|
250
|
+
|
|
251
|
+
return box(content, {
|
|
252
|
+
flexDirection: 'column',
|
|
253
|
+
flexShrink: 0
|
|
254
|
+
});
|
|
244
255
|
}
|