zerg-ztc 0.1.4 → 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 +115 -9
- package/dist/App.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 +2 -1
- package/dist/agent/commands/index.js.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 +46 -31
- 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 +2 -2
- package/dist/components/FullScreen.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 +1 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +2 -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/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 -2
- package/dist/ui/views/app.d.ts.map +1 -1
- package/dist/ui/views/app.js +17 -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 +3 -4
- 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 +2 -1
- package/dist/ui/views/status_bar.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +4 -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 +156 -20
- package/src/agent/commands/config.ts +76 -2
- package/src/agent/commands/index.ts +2 -0
- package/src/agent/commands/update.ts +32 -0
- package/src/cli.tsx +50 -30
- package/src/components/ActivityLine.tsx +23 -0
- package/src/components/FullScreen.tsx +4 -3
- package/src/components/MessageList.tsx +52 -6
- package/src/components/SingleMessage.tsx +59 -0
- package/src/components/StatusBar.tsx +3 -0
- package/src/components/index.tsx +3 -1
- package/src/config/types.ts +1 -0
- package/src/config.ts +8 -0
- package/src/ui/views/activity_line.ts +33 -0
- package/src/ui/views/app.ts +23 -14
- package/src/ui/views/header.ts +3 -4
- 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 +5 -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
package/src/cli.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
-
import { resolve
|
|
5
|
-
import { statSync
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { statSync } from 'fs';
|
|
7
6
|
import { App } from './App.js';
|
|
8
7
|
import { configStore } from './config.js';
|
|
8
|
+
import { getVersion } from './utils/version.js';
|
|
9
9
|
|
|
10
10
|
// --- CLI Entry Point ---
|
|
11
11
|
|
|
@@ -76,15 +76,43 @@ async function main(): Promise<void> {
|
|
|
76
76
|
await configStore.load(true);
|
|
77
77
|
|
|
78
78
|
const isTty = Boolean(process.stdout.isTTY);
|
|
79
|
-
const useAltScreen = isTty
|
|
79
|
+
const useAltScreen = isTty
|
|
80
|
+
&& process.env.ZTC_ALT_SCREEN === '1'
|
|
81
|
+
&& process.env.ZTC_NO_ALT_SCREEN !== '1'
|
|
82
|
+
&& process.env.ZTC_SCROLLBACK !== '1';
|
|
80
83
|
|
|
81
84
|
if (useAltScreen) {
|
|
82
85
|
process.stdout.write('\x1b[?1049h');
|
|
83
86
|
process.stdout.write('\x1b[?25l');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isTty) {
|
|
90
|
+
// Clear screen and move cursor home for a clean start
|
|
84
91
|
process.stdout.write('\x1b[2J');
|
|
85
92
|
process.stdout.write('\x1b[H');
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
// In scrollback mode, print a static header once at startup
|
|
96
|
+
const scrollback = !useAltScreen;
|
|
97
|
+
if (scrollback && isTty) {
|
|
98
|
+
const version = getVersion();
|
|
99
|
+
const dateLabel = new Date().toLocaleDateString('en-US', {
|
|
100
|
+
month: 'short',
|
|
101
|
+
day: '2-digit',
|
|
102
|
+
year: 'numeric'
|
|
103
|
+
});
|
|
104
|
+
const cols = process.stdout.columns || 80;
|
|
105
|
+
const title = 'Zerg Terminal Client';
|
|
106
|
+
const right = `${dateLabel} • v${version} • Ctrl+C exit • /help`;
|
|
107
|
+
const padding = Math.max(0, cols - title.length - right.length - 4);
|
|
108
|
+
const borderLine = '─'.repeat(cols - 2);
|
|
109
|
+
|
|
110
|
+
console.log(`┌${borderLine}┐`);
|
|
111
|
+
console.log(`│ ${title}${' '.repeat(padding)}${right} │`);
|
|
112
|
+
console.log(`└${borderLine}┘`);
|
|
113
|
+
console.log(''); // Extra line after header
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
// Enable bracketed paste mode so we can detect paste boundaries
|
|
89
117
|
process.stdout.write('\x1b[?2004h');
|
|
90
118
|
|
|
@@ -94,21 +122,23 @@ async function main(): Promise<void> {
|
|
|
94
122
|
// Using flags=1 for basic disambiguation which reports Cmd+V as CSI sequence
|
|
95
123
|
process.stdout.write('\x1b[>1u');
|
|
96
124
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
syncPending
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
if (useAltScreen) {
|
|
126
|
+
// Wrap stdout.write to use synchronized output (reduces flicker)
|
|
127
|
+
// DECSM/DECRM 2026 tells terminal to batch updates
|
|
128
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
129
|
+
let syncPending = false;
|
|
130
|
+
process.stdout.write = function(chunk: any, encoding?: any, callback?: any): boolean {
|
|
131
|
+
if (!syncPending) {
|
|
132
|
+
syncPending = true;
|
|
133
|
+
originalWrite('\x1b[?2026h'); // Begin synchronized update
|
|
134
|
+
setImmediate(() => {
|
|
135
|
+
originalWrite('\x1b[?2026l'); // End synchronized update
|
|
136
|
+
syncPending = false;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return originalWrite(chunk, encoding, callback);
|
|
140
|
+
} as typeof process.stdout.write;
|
|
141
|
+
}
|
|
112
142
|
|
|
113
143
|
// Render the app
|
|
114
144
|
const { waitUntilExit } = render(<App />);
|
|
@@ -130,14 +160,4 @@ async function main(): Promise<void> {
|
|
|
130
160
|
|
|
131
161
|
void main();
|
|
132
162
|
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
136
|
-
const pkgPath = resolve(here, '../package.json');
|
|
137
|
-
const raw = readFileSync(pkgPath, 'utf-8');
|
|
138
|
-
const parsed = JSON.parse(raw) as { version?: string };
|
|
139
|
-
return parsed.version || '0.0.0';
|
|
140
|
-
} catch {
|
|
141
|
-
return '0.0.0';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
163
|
+
// getVersion moved to utils/version.ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AgentState } from '../types.js';
|
|
3
|
+
import { InkNode } from '../ui/ink/index.js';
|
|
4
|
+
import { buildActivityLineView } from '../ui/views/activity_line.js';
|
|
5
|
+
|
|
6
|
+
interface ActivityLineProps {
|
|
7
|
+
state: AgentState;
|
|
8
|
+
spinnerLabel?: string | null;
|
|
9
|
+
spinnerFrame?: string | null;
|
|
10
|
+
inputMode?: 'queue' | 'interrupt';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ActivityLine: React.FC<ActivityLineProps> = ({
|
|
14
|
+
state,
|
|
15
|
+
spinnerLabel,
|
|
16
|
+
spinnerFrame,
|
|
17
|
+
inputMode
|
|
18
|
+
}) => {
|
|
19
|
+
const node = buildActivityLineView({ state, spinnerLabel, spinnerFrame, inputMode });
|
|
20
|
+
return <InkNode node={node} />;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default ActivityLine;
|
|
@@ -18,6 +18,7 @@ import { Box } from 'ink';
|
|
|
18
18
|
|
|
19
19
|
interface FullScreenProps extends PropsWithChildren {
|
|
20
20
|
debug?: boolean;
|
|
21
|
+
scrollback?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function getTerminalSize() {
|
|
@@ -27,15 +28,15 @@ function getTerminalSize() {
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export const FullScreen: React.FC<FullScreenProps> = ({ children, debug = false }) => {
|
|
31
|
+
export const FullScreen: React.FC<FullScreenProps> = ({ children, debug = false, scrollback = false }) => {
|
|
31
32
|
const { rows, columns } = useScreenSize();
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
35
|
<Box
|
|
35
36
|
flexDirection="column"
|
|
36
37
|
width={columns}
|
|
37
|
-
height={rows}
|
|
38
|
-
overflow=
|
|
38
|
+
height={scrollback ? undefined : rows}
|
|
39
|
+
overflow={scrollback ? undefined : 'hidden'}
|
|
39
40
|
borderStyle={debug ? 'single' : undefined}
|
|
40
41
|
borderColor={debug ? 'red' : undefined}
|
|
41
42
|
>
|
|
@@ -1,24 +1,70 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef, useMemo } from 'react';
|
|
2
2
|
import { Message } from '../types.js';
|
|
3
3
|
import { InkNode } from '../ui/ink/index.js';
|
|
4
4
|
import { buildMessageListView } from '../ui/views/message_list.js';
|
|
5
5
|
|
|
6
6
|
interface MessageListProps {
|
|
7
7
|
messages: Message[];
|
|
8
|
-
height
|
|
8
|
+
height?: number;
|
|
9
9
|
maxMessages?: number;
|
|
10
10
|
debug?: boolean;
|
|
11
11
|
expandToolOutputs?: boolean;
|
|
12
|
+
scrollback?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const MessageList: React.FC<MessageListProps> = ({
|
|
15
|
-
messages,
|
|
15
|
+
export const MessageList: React.FC<MessageListProps> = ({
|
|
16
|
+
messages,
|
|
16
17
|
height,
|
|
17
18
|
maxMessages = 50,
|
|
18
19
|
debug = false,
|
|
19
|
-
expandToolOutputs = false
|
|
20
|
+
expandToolOutputs = false,
|
|
21
|
+
scrollback = false
|
|
20
22
|
}) => {
|
|
21
|
-
|
|
23
|
+
// In scrollback mode, track which messages have been rendered to avoid duplication
|
|
24
|
+
const renderedIdsRef = useRef<Set<string>>(new Set());
|
|
25
|
+
|
|
26
|
+
// Filter to only new, complete messages in scrollback mode
|
|
27
|
+
const messagesToRender = useMemo(() => {
|
|
28
|
+
if (!scrollback) {
|
|
29
|
+
return messages;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// In scrollback mode, only render messages that:
|
|
33
|
+
// 1. Are NOT currently streaming (wait for completion)
|
|
34
|
+
// 2. Haven't been rendered yet
|
|
35
|
+
const newMessages: Message[] = [];
|
|
36
|
+
for (const msg of messages) {
|
|
37
|
+
// Skip streaming messages - we'll render them when complete
|
|
38
|
+
if (msg.isStreaming) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Skip already rendered messages
|
|
43
|
+
if (renderedIdsRef.current.has(msg.id)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// This is a new, complete message - render it
|
|
48
|
+
newMessages.push(msg);
|
|
49
|
+
// Mark as rendered immediately
|
|
50
|
+
renderedIdsRef.current.add(msg.id);
|
|
51
|
+
}
|
|
52
|
+
return newMessages;
|
|
53
|
+
}, [messages, scrollback]);
|
|
54
|
+
|
|
55
|
+
// Don't render anything if there are no new messages in scrollback mode
|
|
56
|
+
if (scrollback && messagesToRender.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const node = buildMessageListView({
|
|
61
|
+
messages: messagesToRender,
|
|
62
|
+
height,
|
|
63
|
+
maxMessages,
|
|
64
|
+
debug,
|
|
65
|
+
expandToolOutputs,
|
|
66
|
+
scrollback
|
|
67
|
+
});
|
|
22
68
|
return <InkNode node={node} />;
|
|
23
69
|
};
|
|
24
70
|
|
|
@@ -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;
|
|
@@ -15,6 +15,7 @@ interface StatusBarProps {
|
|
|
15
15
|
emulationId?: string;
|
|
16
16
|
inputMode?: 'queue' | 'interrupt';
|
|
17
17
|
toast?: string | null;
|
|
18
|
+
spinnerLabel?: string | null;
|
|
18
19
|
debug?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -30,6 +31,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
30
31
|
emulationId,
|
|
31
32
|
inputMode,
|
|
32
33
|
toast,
|
|
34
|
+
spinnerLabel,
|
|
33
35
|
debug = false
|
|
34
36
|
}) => {
|
|
35
37
|
const node = buildStatusBarView({
|
|
@@ -44,6 +46,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
44
46
|
emulationId,
|
|
45
47
|
inputMode,
|
|
46
48
|
toast,
|
|
49
|
+
spinnerLabel,
|
|
47
50
|
debug
|
|
48
51
|
});
|
|
49
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
|
}
|
|
@@ -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,13 +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
|
-
inputMode?: 'queue' | 'interrupt';
|
|
24
25
|
toast?: string | null;
|
|
26
|
+
spinnerLabel?: string | null;
|
|
27
|
+
spinnerFrame?: string | null;
|
|
28
|
+
inputMode?: 'queue' | 'interrupt';
|
|
25
29
|
debug?: boolean;
|
|
26
30
|
expandToolOutputs?: boolean;
|
|
27
31
|
}
|
|
@@ -48,6 +52,7 @@ export function buildAppView({
|
|
|
48
52
|
rows,
|
|
49
53
|
commands,
|
|
50
54
|
hasApiKey,
|
|
55
|
+
version = '0.1.0',
|
|
51
56
|
contextLength,
|
|
52
57
|
contextEstimated,
|
|
53
58
|
provider,
|
|
@@ -55,17 +60,20 @@ export function buildAppView({
|
|
|
55
60
|
emulationId,
|
|
56
61
|
inputMode,
|
|
57
62
|
toast,
|
|
63
|
+
spinnerLabel,
|
|
64
|
+
spinnerFrame,
|
|
58
65
|
debug = false,
|
|
59
66
|
expandToolOutputs = false
|
|
60
67
|
}: AppViewProps): LayoutNode {
|
|
61
68
|
const suggestionLines = 4;
|
|
62
|
-
const headerHeight =
|
|
69
|
+
const headerHeight = 1;
|
|
63
70
|
const statusHeight = 1;
|
|
71
|
+
const activityHeight = (agentState.status === 'thinking' || agentState.status === 'streaming') && spinnerLabel ? 3 : 0;
|
|
64
72
|
const inputLineCount = estimateInputLines(inputState.segments, cols);
|
|
65
73
|
const previewLines = estimateBadgePreviewLines(inputState);
|
|
66
|
-
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + suggestionLines + 5));
|
|
74
|
+
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + activityHeight + suggestionLines + 5));
|
|
67
75
|
const inputHeight = Math.min(Math.max(1, inputLineCount + previewLines), maxInputLines) + suggestionLines;
|
|
68
|
-
const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight), 5);
|
|
76
|
+
const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight + activityHeight), 5);
|
|
69
77
|
|
|
70
78
|
const placeholder = !hasApiKey ? 'Set API key with /config key <key>' :
|
|
71
79
|
agentState.status === 'thinking' ? 'Thinking...' :
|
|
@@ -74,12 +82,21 @@ export function buildAppView({
|
|
|
74
82
|
'Type a message or /help for commands...';
|
|
75
83
|
|
|
76
84
|
return box([
|
|
77
|
-
buildHeaderView({ version
|
|
85
|
+
buildHeaderView({ version, debug }),
|
|
78
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
|
+
}),
|
|
79
96
|
buildStatusBarView({
|
|
80
97
|
state: agentState,
|
|
81
98
|
sessionId,
|
|
82
|
-
version
|
|
99
|
+
version,
|
|
83
100
|
connectionStatus: hasApiKey ? 'connected' : 'disconnected',
|
|
84
101
|
contextLength,
|
|
85
102
|
contextEstimated,
|
|
@@ -89,14 +106,6 @@ export function buildAppView({
|
|
|
89
106
|
inputMode,
|
|
90
107
|
toast,
|
|
91
108
|
debug
|
|
92
|
-
}),
|
|
93
|
-
buildInputAreaView({
|
|
94
|
-
state: inputState,
|
|
95
|
-
placeholder,
|
|
96
|
-
disabled: agentState.status !== 'idle' && agentState.status !== 'error',
|
|
97
|
-
commands,
|
|
98
|
-
cols,
|
|
99
|
-
debug
|
|
100
109
|
})
|
|
101
110
|
], { flexDirection: 'column' });
|
|
102
111
|
}
|
package/src/ui/views/header.ts
CHANGED
|
@@ -15,7 +15,6 @@ export function buildHeaderView({
|
|
|
15
15
|
showHelp = true,
|
|
16
16
|
debug = false
|
|
17
17
|
}: HeaderProps): LayoutNode {
|
|
18
|
-
// Single-row header to minimize vertical space
|
|
19
18
|
return box([
|
|
20
19
|
box([
|
|
21
20
|
text(title, { bold: true }),
|
|
@@ -37,9 +36,9 @@ export function buildHeaderView({
|
|
|
37
36
|
flexDirection: 'row',
|
|
38
37
|
justifyContent: 'space-between',
|
|
39
38
|
paddingX: 1,
|
|
40
|
-
height: 1,
|
|
41
39
|
flexShrink: 0,
|
|
42
|
-
borderStyle:
|
|
43
|
-
borderColor: debug ? 'cyan' :
|
|
40
|
+
borderStyle: 'single',
|
|
41
|
+
borderColor: debug ? 'cyan' : 'gray',
|
|
42
|
+
marginBottom: 1
|
|
44
43
|
});
|
|
45
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
|
}
|