dave-code 1.0.4 → 1.1.0

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.
@@ -0,0 +1,283 @@
1
+ const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]/g;
2
+ const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
3
+ const STRING_CONTROL_RE = /\x1b[PX^_][\s\S]*?\x1b\\/g;
4
+ const ESCAPE_RE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|.)/g;
5
+
6
+ const COLORS = {
7
+ brand: '\x1b[1;38;2;250;100;30m',
8
+ active: '\x1b[36m',
9
+ success: '\x1b[32m',
10
+ warning: '\x1b[33m',
11
+ error: '\x1b[31m',
12
+ muted: '\x1b[90m',
13
+ reset: '\x1b[0m'
14
+ };
15
+
16
+ export function stripAnsi(text) {
17
+ return String(text || '').replace(ANSI_RE, '');
18
+ }
19
+
20
+ export function sanitizeUntrustedText(text) {
21
+ return String(text ?? '')
22
+ .replace(OSC_RE, '')
23
+ .replace(STRING_CONTROL_RE, '')
24
+ .replace(ESCAPE_RE, '')
25
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '');
26
+ }
27
+
28
+ export function displayWidth(text) {
29
+ let width = 0;
30
+ for (const char of stripAnsi(text)) {
31
+ const code = char.codePointAt(0);
32
+ if (
33
+ (code >= 0x1100 && code <= 0x115f) ||
34
+ (code >= 0x2e80 && code <= 0xa4cf) ||
35
+ (code >= 0xac00 && code <= 0xd7a3) ||
36
+ (code >= 0xf900 && code <= 0xfaff) ||
37
+ (code >= 0xfe10 && code <= 0xfe6f) ||
38
+ (code >= 0xff00 && code <= 0xff60) ||
39
+ (code >= 0x1f300 && code <= 0x1faff)
40
+ ) {
41
+ width += 2;
42
+ } else {
43
+ width += 1;
44
+ }
45
+ }
46
+ return width;
47
+ }
48
+
49
+ export function truncateMiddle(text, maxWidth) {
50
+ const value = String(text || '');
51
+ if (displayWidth(value) <= maxWidth) return value;
52
+ if (maxWidth <= 3) return '.'.repeat(Math.max(0, maxWidth));
53
+ const target = maxWidth - 3;
54
+ const leftTarget = Math.ceil(target / 2);
55
+ const rightTarget = Math.floor(target / 2);
56
+ let left = '';
57
+ let right = '';
58
+ for (const char of value) {
59
+ if (displayWidth(left + char) > leftTarget) break;
60
+ left += char;
61
+ }
62
+ for (const char of [...value].reverse()) {
63
+ if (displayWidth(char + right) > rightTarget) break;
64
+ right = char + right;
65
+ }
66
+ return `${left}...${right}`;
67
+ }
68
+
69
+ function formatDuration(ms) {
70
+ if (!Number.isFinite(ms) || ms < 0) return '';
71
+ if (ms < 1000) return `${Math.round(ms)}ms`;
72
+ return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)}s`;
73
+ }
74
+
75
+ export function createTerminalRenderer({
76
+ stdout = process.stdout,
77
+ lang = 'cn',
78
+ color = true,
79
+ maxCompleted = 8,
80
+ now = Date.now
81
+ } = {}) {
82
+ const isTTY = !!stdout.isTTY;
83
+ const useColor = Boolean(color && isTTY && !process.env.NO_COLOR);
84
+ const useUnicode = Boolean(isTTY && process.env.TERM !== 'dumb');
85
+ const frames = useUnicode
86
+ ? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
87
+ : ['-', '\\', '|', '/'];
88
+ const symbols = useUnicode
89
+ ? { done: '✓', failed: '×', warning: '!', more: '…' }
90
+ : { done: '+', failed: 'x', warning: '!', more: '...' };
91
+ const labels = lang === 'cn'
92
+ ? {
93
+ analyzing: '思考中',
94
+ organizing: '正在整理结果',
95
+ retrying: '流式响应不可用,正在切换兼容模式',
96
+ compacted: '已压缩上下文',
97
+ cancelled: '操作已取消',
98
+ failed: '执行失败',
99
+ extra: '另有步骤已完成'
100
+ }
101
+ : {
102
+ analyzing: 'Thinking',
103
+ organizing: 'Organizing results',
104
+ retrying: 'Streaming unavailable, switching to compatibility mode',
105
+ compacted: 'Context compacted',
106
+ cancelled: 'Operation cancelled',
107
+ failed: 'Turn failed',
108
+ extra: 'additional steps completed'
109
+ };
110
+
111
+ let frameIndex = 0;
112
+ let active = null;
113
+ let timer = null;
114
+ let paused = false;
115
+ let disposed = false;
116
+ let answerOpen = false;
117
+ let answerEndsWithNewline = true;
118
+ let completedCount = 0;
119
+ let hiddenCount = 0;
120
+
121
+ const paint = (name, text) => useColor ? `${COLORS[name]}${text}${COLORS.reset}` : text;
122
+ const write = (text) => stdout.write(String(text));
123
+
124
+ function terminalWidth() {
125
+ return Math.max(24, stdout.columns || 80);
126
+ }
127
+
128
+ function elapsedText() {
129
+ if (!active || terminalWidth() < 58) return '';
130
+ return formatDuration(now() - active.startedAt);
131
+ }
132
+
133
+ function buildActiveText() {
134
+ if (!active) return '';
135
+ const elapsed = elapsedText();
136
+ const suffix = elapsed ? ` · ${elapsed}` : '';
137
+ const available = Math.max(10, terminalWidth() - displayWidth(frames[frameIndex]) - 2);
138
+ return `${frames[frameIndex]} ${truncateMiddle(`${active.label}${suffix}`, available)}`;
139
+ }
140
+
141
+ function drawActive() {
142
+ if (!active || paused || disposed || !isTTY) return;
143
+ write(`\r\x1b[2K${paint('active', buildActiveText())}\x1b[?25l`);
144
+ }
145
+
146
+ function stopTimer() {
147
+ if (timer) clearInterval(timer);
148
+ timer = null;
149
+ }
150
+
151
+ function clearActive({ keep = false } = {}) {
152
+ stopTimer();
153
+ if (isTTY) write('\r\x1b[2K\x1b[?25h');
154
+ if (!keep) active = null;
155
+ }
156
+
157
+ function setActive(label, key = '') {
158
+ if (active && active.key === key && !paused) {
159
+ active.label = label;
160
+ drawActive();
161
+ return;
162
+ }
163
+ clearActive();
164
+ active = { label, key, startedAt: now() };
165
+ if (!isTTY) {
166
+ write(`${label}\n`);
167
+ return;
168
+ }
169
+ drawActive();
170
+ timer = setInterval(() => {
171
+ frameIndex = (frameIndex + 1) % frames.length;
172
+ drawActive();
173
+ }, 90);
174
+ }
175
+
176
+ function commitLine(symbol, text, tone = 'muted') {
177
+ clearActive();
178
+ if (completedCount >= maxCompleted) {
179
+ hiddenCount++;
180
+ return;
181
+ }
182
+ completedCount++;
183
+ const available = Math.max(12, terminalWidth() - displayWidth(symbol) - 2);
184
+ write(`${paint(tone, symbol)} ${truncateMiddle(sanitizeUntrustedText(text), available)}\n`);
185
+ }
186
+
187
+ function finishAnswerLine() {
188
+ if (answerOpen && !answerEndsWithNewline) write('\n');
189
+ answerEndsWithNewline = true;
190
+ }
191
+
192
+ function beginAnswer() {
193
+ if (answerOpen) return;
194
+ clearActive();
195
+ write(`\n${paint('brand', 'Dave')}\n`);
196
+ answerOpen = true;
197
+ answerEndsWithNewline = true;
198
+ }
199
+
200
+ function handle(event) {
201
+ if (!event || disposed) return;
202
+ const data = event.data || {};
203
+
204
+ if (event.type === 'turn.started') {
205
+ setActive(data.label || labels.analyzing, event.type);
206
+ } else if (event.type === 'context.compacted') {
207
+ const detail = data.omittedMessages
208
+ ? (lang === 'cn' ? ` · 省略 ${data.omittedMessages} 条旧消息` : ` · ${data.omittedMessages} older messages omitted`)
209
+ : '';
210
+ commitLine(symbols.done, `${labels.compacted}${detail}`, 'muted');
211
+ } else if (event.type === 'model.started') {
212
+ if (!answerOpen) setActive(data.label || (data.phase === 'finalizing' ? labels.organizing : labels.analyzing), event.type);
213
+ } else if (event.type === 'model.retry') {
214
+ setActive(data.label || labels.retrying, event.type);
215
+ } else if (event.type === 'model.delta') {
216
+ const text = sanitizeUntrustedText(data.text || '');
217
+ if (!text) return;
218
+ clearActive();
219
+ beginAnswer();
220
+ write(text);
221
+ answerEndsWithNewline = text.endsWith('\n');
222
+ } else if (event.type === 'tool.requested') {
223
+ setActive(sanitizeUntrustedText(data.label || data.displaySummary || data.tool || ''), data.toolCallId || event.type);
224
+ } else if (event.type === 'tool.started') {
225
+ setActive(sanitizeUntrustedText(data.label || data.displaySummary || data.tool || ''), data.toolCallId || event.type);
226
+ } else if (event.type === 'tool.progress') {
227
+ const progress = Number.isFinite(data.percent) ? ` · ${data.percent}%` : '';
228
+ setActive(`${sanitizeUntrustedText(data.label || data.displaySummary || data.tool || '')}${progress}`, data.toolCallId || event.type);
229
+ } else if (event.type === 'tool.completed') {
230
+ const prefix = lang === 'cn' ? '工具 · ' : 'Tool · ';
231
+ commitLine(symbols.done, `${prefix}${data.displaySummary || data.tool || 'Done'}`, 'success');
232
+ } else if (event.type === 'tool.failed') {
233
+ commitLine(symbols.failed, data.displaySummary || data.error || data.tool || labels.failed, 'error');
234
+ } else if (event.type === 'permission.requested') {
235
+ clearActive();
236
+ write(`${paint('warning', symbols.warning)} ${sanitizeUntrustedText(data.displaySummary || 'Confirmation required')}\n`);
237
+ if (data.preview) write(`${sanitizeUntrustedText(data.preview).replace(/^\n/, '')}\n`);
238
+ } else if (event.type === 'permission.resolved') {
239
+ if (!data.allowed) commitLine(symbols.warning, data.displaySummary || labels.cancelled, 'warning');
240
+ } else if (event.type === 'turn.failed') {
241
+ finishAnswerLine();
242
+ commitLine(symbols.failed, data.error || labels.failed, 'error');
243
+ } else if (event.type === 'turn.cancelled') {
244
+ finishAnswerLine();
245
+ commitLine(symbols.warning, data.reason || labels.cancelled, 'warning');
246
+ } else if (event.type === 'turn.completed') {
247
+ clearActive();
248
+ finishAnswerLine();
249
+ if (hiddenCount > 0) {
250
+ write(`${paint('muted', symbols.more)} ${hiddenCount} ${labels.extra}\n`);
251
+ }
252
+ if (answerOpen || completedCount > 0) write('\n');
253
+ }
254
+ }
255
+
256
+ function pause() {
257
+ paused = true;
258
+ clearActive({ keep: true });
259
+ }
260
+
261
+ function resume() {
262
+ if (disposed) return;
263
+ paused = false;
264
+ if (active) {
265
+ drawActive();
266
+ if (isTTY && !timer) {
267
+ timer = setInterval(() => {
268
+ frameIndex = (frameIndex + 1) % frames.length;
269
+ drawActive();
270
+ }, 90);
271
+ }
272
+ }
273
+ }
274
+
275
+ function dispose() {
276
+ if (disposed) return;
277
+ clearActive();
278
+ finishAnswerLine();
279
+ disposed = true;
280
+ }
281
+
282
+ return { handle, pause, resume, dispose };
283
+ }