xiaotime 0.5.1 → 0.5.3

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 (50) hide show
  1. package/dist/__tests__/ansi_tags.test.d.ts +9 -0
  2. package/dist/__tests__/ansi_tags.test.d.ts.map +1 -0
  3. package/dist/__tests__/ansi_tags.test.js +58 -0
  4. package/dist/__tests__/ansi_tags.test.js.map +1 -0
  5. package/dist/__tests__/paste_transform_submit.test.d.ts +2 -0
  6. package/dist/__tests__/paste_transform_submit.test.d.ts.map +1 -0
  7. package/dist/__tests__/paste_transform_submit.test.js +74 -0
  8. package/dist/__tests__/paste_transform_submit.test.js.map +1 -0
  9. package/dist/__tests__/session_match.test.d.ts +10 -0
  10. package/dist/__tests__/session_match.test.d.ts.map +1 -0
  11. package/dist/__tests__/session_match.test.js +55 -0
  12. package/dist/__tests__/session_match.test.js.map +1 -0
  13. package/dist/ansi_tags.d.ts +20 -0
  14. package/dist/ansi_tags.d.ts.map +1 -0
  15. package/dist/ansi_tags.js +38 -0
  16. package/dist/ansi_tags.js.map +1 -0
  17. package/dist/config.d.ts +1 -1
  18. package/dist/config.js +1 -1
  19. package/dist/display.d.ts +53 -22
  20. package/dist/display.d.ts.map +1 -1
  21. package/dist/display.js +773 -47
  22. package/dist/display.js.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +34 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/session.d.ts.map +1 -1
  28. package/dist/session.js +5 -2
  29. package/dist/session.js.map +1 -1
  30. package/dist/session_match.d.ts +19 -0
  31. package/dist/session_match.d.ts.map +1 -0
  32. package/dist/session_match.js +9 -0
  33. package/dist/session_match.js.map +1 -0
  34. package/dist/sidebar.d.ts +84 -0
  35. package/dist/sidebar.d.ts.map +1 -0
  36. package/dist/sidebar.js +310 -0
  37. package/dist/sidebar.js.map +1 -0
  38. package/dist/tools.d.ts +4 -0
  39. package/dist/tools.d.ts.map +1 -1
  40. package/dist/tools.js +25 -66
  41. package/dist/tools.js.map +1 -1
  42. package/dist/ui_state.d.ts +41 -96
  43. package/dist/ui_state.d.ts.map +1 -1
  44. package/dist/ui_state.js +212 -209
  45. package/dist/ui_state.js.map +1 -1
  46. package/dist/ws.d.ts +10 -14
  47. package/dist/ws.d.ts.map +1 -1
  48. package/dist/ws.js +236 -119
  49. package/dist/ws.js.map +1 -1
  50. package/package.json +10 -5
package/dist/display.js CHANGED
@@ -1,81 +1,807 @@
1
1
  "use strict";
2
2
  /**
3
- * Output formatting — streaming text, session list, cost display.
3
+ * Output formatting — streaming text, session list, status bar, tool lines.
4
+ *
5
+ * SPRINT-CLI-UX-POLISH-5:
6
+ * - Thinking spinner (animates between submit and first token)
7
+ * - Animated tool lines (spinner → ✓ with elapsed on completion)
8
+ * - Diff view for write_file tool calls
9
+ * - Approval prompt styled as yellow bordered box
10
+ * - Keyboard shortcut bar shown ONCE on welcome only
11
+ * - Session resume shows last message preview
12
+ * - "Xiaotime Labs" wordmark in status bar
13
+ * - Word-wrap at terminal width (no mid-word cuts)
14
+ * - Code fence double-render fix
15
+ * - Sticky bottom status bar (no bg, connected dot, turn counter, last tool)
16
+ * - No cost in status bar
17
+ * - Double-border input box (╭─╮ top, you › inside, ╰─╯ on submit)
18
+ * - Red user/Junior text (#c0384b)
19
+ * - Claude Code-style turn headers (Junior ›)
20
+ * - Code blocks with full-width border + language tag
21
+ * - Turn separators between responses
22
+ * - Tool call lines with elapsed time + icons
23
+ *
24
+ * SPRINT-CLI-UX-POLISH-6:
25
+ * - Restored "Junior is thinking..." label.
26
+ * - Inline code backtick spans get subtle background tint
27
+ * - Auto-detect file paths in output — underlined + OSC 8 hyperlinks (iTerm2/Termius)
28
+ * - Tool call errors render as red-bordered box instead of inline text
29
+ * - Removed "Junior is thinking..." spinner label
4
30
  */
5
31
  var __importDefault = (this && this.__importDefault) || function (mod) {
6
32
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
33
  };
8
34
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.printWelcome = printWelcome;
35
+ exports.initStatusBar = initStatusBar;
36
+ exports.setConnected = setConnected;
37
+ exports.incrementTurn = incrementTurn;
38
+ exports.setLastTool = setLastTool;
39
+ exports.updateStatusBarCost = updateStatusBarCost;
40
+ exports.startSpinner = startSpinner;
41
+ exports.stopSpinner = stopSpinner;
42
+ exports.startToolLine = startToolLine;
43
+ exports.finishToolLine = finishToolLine;
44
+ exports.printToolError = printToolError;
45
+ exports.printFileDiff = printFileDiff;
46
+ exports.printApprovalPrompt = printApprovalPrompt;
10
47
  exports.printTextDelta = printTextDelta;
48
+ exports.printTurnStart = printTurnStart;
49
+ exports.printThinkingStart = printThinkingStart;
50
+ exports.printThinkingStop = printThinkingStop;
11
51
  exports.printTurnEnd = printTurnEnd;
52
+ exports.consumeFirstPrompt = consumeFirstPrompt;
53
+ exports.printPrompt = printPrompt;
54
+ exports.printPromptEnd = printPromptEnd;
55
+ exports.printWelcome = printWelcome;
12
56
  exports.printError = printError;
13
57
  exports.printSessionList = printSessionList;
14
- exports.printPrompt = printPrompt;
58
+ exports.friendlyToolLabel = friendlyToolLabel;
59
+ exports.friendlyError = friendlyError;
60
+ exports.printHelp = printHelp;
61
+ exports.printDisconnectBanner = printDisconnectBanner;
62
+ exports.printExitConfirmation = printExitConfirmation;
63
+ exports.printToolCallLine = printToolCallLine;
15
64
  exports.printServerToolCall = printServerToolCall;
16
65
  const chalk_1 = __importDefault(require("chalk"));
17
- function printWelcome(sessionId, sessionName, resumed, messageCount) {
18
- const name = sessionName || sessionId.slice(0, 8);
19
- if (resumed) {
20
- console.log(chalk_1.default.cyan(`\n↩ Resumed: ${name} (${messageCount} previous turns)`));
66
+ const cli_highlight_1 = require("cli-highlight");
67
+ // Force color output Termius and some SSH clients report level 0
68
+ // even when the terminal supports 256 colors.
69
+ if (chalk_1.default.level === 0 &&
70
+ (process.env.TERM?.includes("256color") ||
71
+ process.env.TERM?.includes("xterm") ||
72
+ process.env.COLORTERM ||
73
+ process.env.FORCE_COLOR)) {
74
+ chalk_1.default.level = 2;
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Terminal geometry helpers
78
+ // ---------------------------------------------------------------------------
79
+ function termWidth() {
80
+ return process.stdout.columns || 80;
81
+ }
82
+ function termRows() {
83
+ return process.stdout.rows || 24;
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Word wrap — break at word boundaries, never mid-word
87
+ // ---------------------------------------------------------------------------
88
+ function wordWrap(text, width) {
89
+ if (width <= 0)
90
+ return [text];
91
+ const words = text.split(" ");
92
+ const lines = [];
93
+ let current = "";
94
+ for (const word of words) {
95
+ if (word.length >= width) {
96
+ if (current.length > 0) {
97
+ lines.push(current);
98
+ current = "";
99
+ }
100
+ lines.push(word);
101
+ continue;
102
+ }
103
+ const candidate = current.length === 0 ? word : current + " " + word;
104
+ if (candidate.length > width) {
105
+ lines.push(current);
106
+ current = word;
107
+ }
108
+ else {
109
+ current = candidate;
110
+ }
111
+ }
112
+ if (current.length > 0)
113
+ lines.push(current);
114
+ return lines;
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // OSC 8 hyperlink helper (iTerm2, Termius, WezTerm)
118
+ // ---------------------------------------------------------------------------
119
+ function osc8Link(url, text) {
120
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
121
+ }
122
+ // Regex: absolute paths (/foo/bar), relative with ./ or ../, or paths with
123
+ // a common extension — conservative to avoid false positives in prose.
124
+ const FILE_PATH_RE = /(?<![`\w])((?:\/[\w.\-]+)+(?:\/[\w.\-]+)?|\.{1,2}\/[\w.\-/]+|[\w.\-]+\/[\w.\-]+\.(?:ts|js|tsx|jsx|json|md|py|go|rs|sh|yaml|yml|env|toml|lock))(?![`\w])/g;
125
+ function _linkifyPaths(text) {
126
+ return text.replace(FILE_PATH_RE, (match) => {
127
+ const url = match.startsWith("/") ? `file://${match}` : `file://${process.cwd()}/${match}`;
128
+ return chalk_1.default.underline(osc8Link(url, match));
129
+ });
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Lightweight markdown renderer — chalk only, no external markdown deps
133
+ // ---------------------------------------------------------------------------
134
+ function renderMarkdownLine(line) {
135
+ const h3 = line.match(/^### (.+)$/);
136
+ if (h3)
137
+ return chalk_1.default.bold.hex("#94e2d5")(h3[1]);
138
+ const h2 = line.match(/^## (.+)$/);
139
+ if (h2)
140
+ return chalk_1.default.bold.hex("#89b4fa")(h2[1]);
141
+ const h1 = line.match(/^# (.+)$/);
142
+ if (h1)
143
+ return chalk_1.default.bold.hex("#c0384b")(h1[1]);
144
+ if (/^---+$/.test(line) || /^\*\*\*+$/.test(line)) {
145
+ return chalk_1.default.dim("─".repeat(termWidth()));
146
+ }
147
+ if (line.startsWith("> ")) {
148
+ return chalk_1.default.dim.italic(" │ " + line.slice(2));
149
+ }
150
+ const bullet = line.match(/^(\s*)[*\-+] (.+)$/);
151
+ if (bullet) {
152
+ return `${bullet[1]}${chalk_1.default.hex("#f38ba8")("•")} ${_renderInline(bullet[2])}`;
153
+ }
154
+ const numbered = line.match(/^(\s*)(\d+)\. (.+)$/);
155
+ if (numbered) {
156
+ return `${numbered[1]}${chalk_1.default.dim(numbered[2] + ".")} ${_renderInline(numbered[3])}`;
157
+ }
158
+ return _renderInline(line);
159
+ }
160
+ function _renderInline(text) {
161
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, (_m, t) => chalk_1.default.bold.italic(t));
162
+ text = text.replace(/\*\*(.+?)\*\*/g, (_m, t) => chalk_1.default.bold(t));
163
+ text = text.replace(/\*(.+?)\*/g, (_m, t) => chalk_1.default.italic(t));
164
+ text = text.replace(/_(.+?)_/g, (_m, t) => chalk_1.default.italic(t));
165
+ // Inline code — subtle background tint (#313244 = Catppuccin surface0, text stays readable)
166
+ text = text.replace(/`([^`]+)`/g, (_m, t) => chalk_1.default.bgHex("#313244").hex("#cdd6f4")(` ${t} `));
167
+ text = text.replace(/\[(.+?)\]\((.+?)\)/g, (_m, label, url) => `${chalk_1.default.hex("#89b4fa")(label)} ${chalk_1.default.dim(`(${url})`)}`);
168
+ // Linkify file paths (only outside code spans — already consumed above)
169
+ text = _linkifyPaths(text);
170
+ return text;
171
+ }
172
+ let _statusBar = {
173
+ tenant: process.env.XIAOTIME_TENANT_ID ??
174
+ process.env.TENANT_ID ??
175
+ "craig",
176
+ sessionId: "",
177
+ sessionName: null,
178
+ connected: false,
179
+ turnCount: 0,
180
+ lastTool: null,
181
+ };
182
+ /** Naively strip ANSI codes for length calculation. */
183
+ function stripAnsi(s) {
184
+ // eslint-disable-next-line no-control-regex
185
+ return s.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\]8;;.*?\x07.*?\x1b\]8;;\x07/g, (m) => {
186
+ // Extract visible text from OSC 8 links for length calc
187
+ const inner = m.match(/\x07(.+?)\x1b/);
188
+ return inner ? inner[1] : "";
189
+ });
190
+ }
191
+ function _renderStatusBarLine() {
192
+ const w = termWidth();
193
+ const dot = _statusBar.connected
194
+ ? chalk_1.default.hex("#a6e3a1")("●")
195
+ : chalk_1.default.dim("○");
196
+ const tenant = chalk_1.default.hex("#c0384b")(_statusBar.tenant);
197
+ const session = _statusBar.sessionName
198
+ ? chalk_1.default.hex("#89b4fa")(_statusBar.sessionName)
199
+ : chalk_1.default.dim(_statusBar.sessionId.slice(0, 8) || "…");
200
+ const turns = chalk_1.default.dim(`${_statusBar.turnCount} turns`);
201
+ const lastTool = _statusBar.lastTool
202
+ ? chalk_1.default.dim(`↳ ${_statusBar.lastTool}`)
203
+ : chalk_1.default.dim("ready");
204
+ const sep = chalk_1.default.dim(" · ");
205
+ const left = ` ${dot} ${tenant}${sep}${session}${sep}${turns}${sep}${lastTool}`;
206
+ const right = chalk_1.default.hex("#c0384b")("Xiaotime Labs") + " ";
207
+ const leftRaw = stripAnsi(left);
208
+ const rightRaw = stripAnsi(right);
209
+ const pad = Math.max(0, w - leftRaw.length - rightRaw.length);
210
+ return left + " ".repeat(pad) + right;
211
+ }
212
+ // Reserve the bottom row for the status bar via a DECSTBM scroll region so
213
+ // normal output scrolls only in rows 1..rows-1 and the bar stays pinned. Without
214
+ // this the absolute-positioned bar leaked a copy into scrollback every time the
215
+ // screen scrolled — which is why "Xiaotime Labs" repeated up the window instead
216
+ // of staying sticky at the bottom.
217
+ let _scrollRegionActive = false;
218
+ function _setScrollRegion() {
219
+ const rows = termRows();
220
+ const bottom = Math.max(1, rows - 1);
221
+ process.stdout.write(`\x1b[1;${bottom}r`);
222
+ // DECSTBM homes the cursor to the top margin on some terminals; drop it back
223
+ // to the bottom of the region so output continues where it left off.
224
+ process.stdout.write(`\x1b[${bottom};1H`);
225
+ _scrollRegionActive = true;
226
+ }
227
+ function _resetScrollRegion() {
228
+ if (!_scrollRegionActive)
229
+ return;
230
+ process.stdout.write("\x1b[r"); // restore full-screen scroll region
231
+ _scrollRegionActive = false;
232
+ }
233
+ function _drawStickyBar() {
234
+ const rows = termRows();
235
+ const bar = _renderStatusBarLine();
236
+ process.stdout.write(`\x1b[s` +
237
+ `\x1b[${rows};0H` +
238
+ `\x1b[2K` +
239
+ bar +
240
+ `\x1b[u`);
241
+ }
242
+ process.stdout.on("resize", () => {
243
+ _setScrollRegion();
244
+ _drawStickyBar();
245
+ });
246
+ // Always tear the scroll region down on exit so the shell prompt isn't trapped
247
+ // in the top region after the CLI quits.
248
+ process.on("exit", () => {
249
+ _resetScrollRegion();
250
+ process.stdout.write("\x1b[?25h");
251
+ });
252
+ function initStatusBar(sessionId, sessionName, tenant) {
253
+ _statusBar.sessionId = sessionId;
254
+ _statusBar.sessionName = sessionName;
255
+ if (tenant)
256
+ _statusBar.tenant = tenant;
257
+ _statusBar.connected = false;
258
+ _setScrollRegion();
259
+ _drawStickyBar();
260
+ }
261
+ function setConnected(connected) {
262
+ _statusBar.connected = connected;
263
+ _drawStickyBar();
264
+ }
265
+ function incrementTurn() {
266
+ _statusBar.turnCount += 1;
267
+ _drawStickyBar();
268
+ }
269
+ function setLastTool(toolName) {
270
+ _statusBar.lastTool = toolName;
271
+ _drawStickyBar();
272
+ }
273
+ function updateStatusBarCost(_turnCostUsd) {
274
+ // no-op — cost not shown in bar
275
+ }
276
+ // ---------------------------------------------------------------------------
277
+ // Thinking spinner
278
+ // ---------------------------------------------------------------------------
279
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
280
+ let _spinnerInterval = null;
281
+ let _spinnerFrame = 0;
282
+ let _spinnerLabel = "";
283
+ let _spinnerActive = false;
284
+ function _clearSpinnerLine() {
285
+ process.stdout.write("\x1b[2K\r");
286
+ }
287
+ function startSpinner(label) {
288
+ if (_spinnerActive)
289
+ stopSpinner();
290
+ _spinnerLabel = label;
291
+ _spinnerActive = true;
292
+ _spinnerFrame = 0;
293
+ _spinnerInterval = setInterval(() => {
294
+ if (!_spinnerActive)
295
+ return;
296
+ const frame = chalk_1.default.hex("#c0384b")(SPINNER_FRAMES[_spinnerFrame % SPINNER_FRAMES.length]);
297
+ _spinnerFrame++;
298
+ const labelPart = _spinnerLabel ? ` ${chalk_1.default.dim(_spinnerLabel)}` : "";
299
+ process.stdout.write(`\r ${frame}${labelPart}`);
300
+ }, 80);
301
+ }
302
+ function stopSpinner(finalLabel, success = true) {
303
+ if (_spinnerInterval) {
304
+ clearInterval(_spinnerInterval);
305
+ _spinnerInterval = null;
306
+ }
307
+ _spinnerActive = false;
308
+ _clearSpinnerLine();
309
+ if (finalLabel) {
310
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
311
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(finalLabel)}\n`);
312
+ }
313
+ }
314
+ let _activeTool = null;
315
+ function startToolLine(name, detail) {
316
+ if (_activeTool)
317
+ finishToolLine(true);
318
+ setLastTool(name);
319
+ let frame = 0;
320
+ const spinnerInterval = setInterval(() => {
321
+ const f = chalk_1.default.hex("#89b4fa")(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
322
+ const friendly = friendlyToolLabel(name);
323
+ const detailStr = detail ? chalk_1.default.dim(` ${detail}`) : "";
324
+ process.stdout.write(`\r ${f} ${chalk_1.default.dim(friendly)}${detailStr}`);
325
+ frame++;
326
+ }, 80);
327
+ _activeTool = {
328
+ name,
329
+ detail,
330
+ startedAt: Date.now(),
331
+ spinnerInterval,
332
+ frame: 0,
333
+ };
334
+ }
335
+ function finishToolLine(success = true, errorMessage) {
336
+ if (!_activeTool)
337
+ return;
338
+ clearInterval(_activeTool.spinnerInterval);
339
+ const elapsed = Date.now() - _activeTool.startedAt;
340
+ if (!success && errorMessage) {
341
+ // Clear the spinner line, then render the red error box
342
+ process.stdout.write(`\r\x1b[2K`);
343
+ printToolError(_activeTool.name, errorMessage, elapsed);
21
344
  }
22
345
  else {
23
- console.log(chalk_1.default.cyan(`\n✦ New session: ${name}`));
346
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
347
+ const detailStr = _activeTool.detail ? chalk_1.default.dim(` ${_activeTool.detail}`) : "";
348
+ const dur = chalk_1.default.dim(` ${elapsed}ms`);
349
+ const friendlyLabel = friendlyToolLabel(_activeTool.name);
350
+ process.stdout.write(`\r\x1b[2K ${icon} ${chalk_1.default.dim(friendlyLabel)}${detailStr}${dur}\n`);
24
351
  }
25
- console.log(chalk_1.default.dim("Type your message. Ctrl+C to exit.\n"));
352
+ _activeTool = null;
26
353
  }
354
+ // ---------------------------------------------------------------------------
355
+ // Tool error — red bordered box
356
+ // ---------------------------------------------------------------------------
357
+ function printToolError(toolName, errorMessage, durationMs) {
358
+ const w = termWidth();
359
+ const top = chalk_1.default.hex("#f38ba8")("╭") +
360
+ chalk_1.default.hex("#f38ba8")("─".repeat(w - 2)) +
361
+ chalk_1.default.hex("#f38ba8")("╮");
362
+ const bottom = chalk_1.default.hex("#f38ba8")("╰") +
363
+ chalk_1.default.hex("#f38ba8")("─".repeat(w - 2)) +
364
+ chalk_1.default.hex("#f38ba8")("╯");
365
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
366
+ const titleLine = chalk_1.default.bold.hex("#f38ba8")(` ✗ ${toolName}${dur}`);
367
+ const bodyLines = wordWrap(errorMessage, w - 4).map((l) => chalk_1.default.hex("#f38ba8")(" " + l));
368
+ process.stdout.write("\n" + top + "\n");
369
+ process.stdout.write(titleLine + "\n");
370
+ for (const l of bodyLines) {
371
+ process.stdout.write(l + "\n");
372
+ }
373
+ process.stdout.write(bottom + "\n\n");
374
+ }
375
+ // ---------------------------------------------------------------------------
376
+ // Diff view for write_file
377
+ // ---------------------------------------------------------------------------
378
+ function printFileDiff(path, before, after) {
379
+ const w = termWidth();
380
+ const label = chalk_1.default.dim(` write_file ${path} `);
381
+ const labelRaw = ` write_file ${path} `;
382
+ const rule = chalk_1.default.dim("─".repeat(Math.max(0, w - labelRaw.length)));
383
+ process.stdout.write("\n" + rule + label + "\n");
384
+ if (!before) {
385
+ // New file — show all lines as added
386
+ const lines = after.split("\n").slice(0, 20);
387
+ for (const line of lines) {
388
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + line) + "\n");
389
+ }
390
+ if (after.split("\n").length > 20) {
391
+ process.stdout.write(chalk_1.default.dim(` … ${after.split("\n").length - 20} more lines\n`));
392
+ }
393
+ }
394
+ else {
395
+ const beforeLines = before.split("\n");
396
+ const afterLines = after.split("\n");
397
+ let shown = 0;
398
+ const maxLines = 30;
399
+ for (let i = 0; i < Math.max(beforeLines.length, afterLines.length) && shown < maxLines; i++) {
400
+ const b = beforeLines[i];
401
+ const a = afterLines[i];
402
+ if (b === undefined) {
403
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + a) + "\n");
404
+ shown++;
405
+ }
406
+ else if (a === undefined) {
407
+ process.stdout.write(chalk_1.default.hex("#f38ba8")("- " + b) + "\n");
408
+ shown++;
409
+ }
410
+ else if (b !== a) {
411
+ process.stdout.write(chalk_1.default.hex("#f38ba8")("- " + b) + "\n");
412
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + a) + "\n");
413
+ shown += 2;
414
+ }
415
+ }
416
+ const totalDiff = Math.abs(afterLines.length - beforeLines.length);
417
+ if (totalDiff > maxLines) {
418
+ process.stdout.write(chalk_1.default.dim(` … ${totalDiff - maxLines} more changes\n`));
419
+ }
420
+ }
421
+ process.stdout.write(chalk_1.default.dim("─".repeat(w)) + "\n\n");
422
+ }
423
+ // ---------------------------------------------------------------------------
424
+ // Approval prompt — yellow bordered box
425
+ // ---------------------------------------------------------------------------
426
+ function printApprovalPrompt(title, body) {
427
+ const w = termWidth();
428
+ const top = chalk_1.default.hex("#f9e2af")("╭") +
429
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
430
+ chalk_1.default.hex("#f9e2af")("╮");
431
+ const bottom = chalk_1.default.hex("#f9e2af")("╰") +
432
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
433
+ chalk_1.default.hex("#f9e2af")("╯");
434
+ const titleLine = chalk_1.default.bold.hex("#f9e2af")(" " + title);
435
+ const bodyLines = wordWrap(body, w - 4).map((l) => chalk_1.default.hex("#f9e2af")(" " + l));
436
+ process.stdout.write("\n" + top + "\n");
437
+ process.stdout.write(titleLine + "\n");
438
+ for (const l of bodyLines) {
439
+ process.stdout.write(l + "\n");
440
+ }
441
+ process.stdout.write(bottom + "\n");
442
+ }
443
+ // ---------------------------------------------------------------------------
444
+ // Streaming buffer — handle code fences in real-time, markdown on flush
445
+ // ---------------------------------------------------------------------------
446
+ let _streamBuf = "";
447
+ let _inCodeFence = false;
448
+ let _codeFenceLang = "";
449
+ let _codeFenceBuffer = [];
450
+ let _turnHasContent = false;
27
451
  function printTextDelta(text) {
28
- process.stdout.write(text);
452
+ // Stop spinner on first token
453
+ if (_spinnerActive)
454
+ stopSpinner();
455
+ if (_activeTool)
456
+ finishToolLine(true);
457
+ _streamBuf += text;
458
+ const lines = _streamBuf.split("\n");
459
+ const complete = lines.slice(0, -1);
460
+ _streamBuf = lines[lines.length - 1];
461
+ for (const line of complete) {
462
+ _writeLine(line);
463
+ }
464
+ }
465
+ function _writeLine(line) {
466
+ _turnHasContent = true;
467
+ if (!_inCodeFence) {
468
+ const fenceMatch = line.match(/^```(\w*)$/);
469
+ if (fenceMatch) {
470
+ _inCodeFence = true;
471
+ _codeFenceLang = fenceMatch[1] || "plaintext";
472
+ _codeFenceBuffer = [];
473
+ const w = termWidth();
474
+ const lang = _codeFenceLang !== "plaintext" ? _codeFenceLang : "";
475
+ const label = lang ? chalk_1.default.dim(` ${lang} `) : "";
476
+ const labelRaw = lang ? ` ${lang} ` : "";
477
+ const rule = chalk_1.default.dim("─".repeat(Math.max(0, w - labelRaw.length)));
478
+ process.stdout.write(rule + label + "\n");
479
+ return;
480
+ }
481
+ const w = termWidth();
482
+ const rendered = renderMarkdownLine(line);
483
+ const rawLen = stripAnsi(rendered).length;
484
+ if (rawLen <= w) {
485
+ process.stdout.write(rendered + "\n");
486
+ }
487
+ else {
488
+ const segments = wordWrap(line, w);
489
+ for (const seg of segments) {
490
+ process.stdout.write(renderMarkdownLine(seg) + "\n");
491
+ }
492
+ }
493
+ }
494
+ else {
495
+ if (line === "```") {
496
+ const code = _codeFenceBuffer.join("\n");
497
+ const language = _codeFenceLang && (0, cli_highlight_1.supportsLanguage)(_codeFenceLang)
498
+ ? _codeFenceLang
499
+ : "plaintext";
500
+ try {
501
+ process.stdout.write((0, cli_highlight_1.highlight)(code, { language, ignoreIllegals: true }) + "\n");
502
+ }
503
+ catch {
504
+ process.stdout.write(chalk_1.default.dim(code) + "\n");
505
+ }
506
+ process.stdout.write(chalk_1.default.dim("─".repeat(termWidth())) + "\n");
507
+ _inCodeFence = false;
508
+ _codeFenceLang = "";
509
+ _codeFenceBuffer = [];
510
+ }
511
+ else {
512
+ _codeFenceBuffer.push(line);
513
+ }
514
+ }
515
+ }
516
+ // ---------------------------------------------------------------------------
517
+ // Turn lifecycle
518
+ // ---------------------------------------------------------------------------
519
+ function printTurnStart() {
520
+ process.stdout.write("\x1b[?25l"); // hide cursor
521
+ process.stdout.write("\n" +
522
+ chalk_1.default.hex("#c0384b")("Junior") +
523
+ chalk_1.default.dim(" › ") +
524
+ "\n");
525
+ _turnHasContent = false;
526
+ }
527
+ function printThinkingStart() {
528
+ startSpinner("");
529
+ }
530
+ function printThinkingStop() {
531
+ stopSpinner();
29
532
  }
30
533
  function printTurnEnd(costUsd) {
31
- const costStr = costUsd > 0 ? chalk_1.default.dim(` [$${costUsd.toFixed(4)}]`) : "";
32
- process.stdout.write(`\n${costStr}\n\n`);
534
+ process.stdout.write("\x1b[?25h"); // restore cursor
535
+ // Stop any lingering spinner or tool line
536
+ if (_spinnerActive)
537
+ stopSpinner();
538
+ if (_activeTool)
539
+ finishToolLine(true);
540
+ if (_streamBuf.length > 0) {
541
+ _writeLine(_streamBuf);
542
+ _streamBuf = "";
543
+ }
544
+ _inCodeFence = false;
545
+ _codeFenceBuffer = [];
546
+ incrementTurn();
547
+ process.stdout.write("\n" + chalk_1.default.dim("─".repeat(termWidth())) + "\n");
548
+ }
549
+ // ---------------------------------------------------------------------------
550
+ // Input prompt — top border only, no repeated shortcut bar
551
+ // ---------------------------------------------------------------------------
552
+ // ---------------------------------------------------------------------------
553
+ // First-launch state — show placeholder hint on very first prompt
554
+ // ---------------------------------------------------------------------------
555
+ let _isFirstPrompt = true;
556
+ function consumeFirstPrompt() {
557
+ const was = _isFirstPrompt;
558
+ _isFirstPrompt = false;
559
+ return was;
560
+ }
561
+ function printPrompt(showHint = false) {
562
+ const w = termWidth();
563
+ const topBorder = chalk_1.default.dim("╭") +
564
+ chalk_1.default.dim("─".repeat(w - 2)) +
565
+ chalk_1.default.dim("╮");
566
+ const hint = showHint ? chalk_1.default.dim(" Type a message, or \'help\' for examples") : "";
567
+ process.stdout.write("\n" +
568
+ topBorder + "\n" +
569
+ chalk_1.default.bold.hex("#c0384b")("you") +
570
+ chalk_1.default.dim(" › ") +
571
+ hint);
572
+ }
573
+ function printPromptEnd() {
574
+ const w = termWidth();
575
+ const bottomBorder = chalk_1.default.dim("╰") +
576
+ chalk_1.default.dim("─".repeat(w - 2)) +
577
+ chalk_1.default.dim("╯");
578
+ process.stdout.write("\n" + bottomBorder + "\n");
579
+ }
580
+ // ---------------------------------------------------------------------------
581
+ // Welcome + session list
582
+ // ---------------------------------------------------------------------------
583
+ function printWelcome(sessionId, sessionName, resumed, messageCount, lastMessagePreview) {
584
+ initStatusBar(sessionId, sessionName);
585
+ const name = sessionName || sessionId.slice(0, 8);
586
+ // Shortcut bar — shown ONCE on welcome only
587
+ const w = termWidth();
588
+ const shortcuts = [
589
+ chalk_1.default.dim("ctrl+c") + " " + chalk_1.default.hex("#6c7086")("exit"),
590
+ chalk_1.default.dim("ctrl+r") + " " + chalk_1.default.hex("#6c7086")("sessions"),
591
+ chalk_1.default.dim("ctrl+l") + " " + chalk_1.default.hex("#6c7086")("clear"),
592
+ chalk_1.default.dim("↑↓") + " " + chalk_1.default.hex("#6c7086")("history"),
593
+ ].join(chalk_1.default.dim(" · "));
594
+ const shortcutRaw = stripAnsi(shortcuts);
595
+ const shortcutPad = Math.max(0, Math.floor((w - shortcutRaw.length) / 2));
596
+ if (resumed) {
597
+ process.stdout.write("\n" +
598
+ chalk_1.default.hex("#89b4fa")("↩ " + name) +
599
+ chalk_1.default.dim(` ${messageCount} turns`) +
600
+ "\n");
601
+ if (lastMessagePreview) {
602
+ const truncated = lastMessagePreview.length > 60
603
+ ? lastMessagePreview.slice(0, 60) + "…"
604
+ : lastMessagePreview;
605
+ process.stdout.write(chalk_1.default.dim(' last: "' + truncated + '"') + "\n");
606
+ }
607
+ process.stdout.write("\n" +
608
+ " ".repeat(shortcutPad) + shortcuts + "\n\n");
609
+ }
610
+ else {
611
+ process.stdout.write("\n" +
612
+ chalk_1.default.hex("#c0384b").bold("✦ Junior") +
613
+ chalk_1.default.dim(` ${name}`) +
614
+ "\n\n" +
615
+ " ".repeat(shortcutPad) + shortcuts + "\n\n");
616
+ }
33
617
  }
34
618
  function printError(message) {
35
- console.error(chalk_1.default.red(`\nError: ${message}\n`));
619
+ process.stderr.write(chalk_1.default.red("\n✗ ") +
620
+ chalk_1.default.bold.red("Error: ") +
621
+ chalk_1.default.red(message) +
622
+ "\n\n");
36
623
  }
37
624
  function printSessionList(sessions) {
38
625
  if (sessions.length === 0) {
39
626
  console.log(chalk_1.default.dim("No sessions found."));
40
627
  return;
41
628
  }
42
- console.log(chalk_1.default.bold("\nSessions:\n"));
629
+ const w = termWidth();
630
+ process.stdout.write("\n" +
631
+ chalk_1.default.bold.hex("#c0384b")("Sessions") +
632
+ "\n" +
633
+ chalk_1.default.dim("─".repeat(w)) +
634
+ "\n");
43
635
  for (const s of sessions) {
44
- const name = s.session_name || chalk_1.default.dim("(unnamed)");
45
- const id = chalk_1.default.dim(s.id.slice(0, 8));
46
- const turns = `${s.message_count} turns`;
47
- const cost = `$${Number(s.total_cost_usd).toFixed(4)}`;
48
- const lastActive = new Date(s.last_active_at).toLocaleString();
49
- const status = s.status === "active" ? chalk_1.default.green("●") : chalk_1.default.dim("○");
50
- console.log(` ${status} ${name} ${id}`);
51
- console.log(chalk_1.default.dim(` ${turns} · ${cost} · last active ${lastActive}`));
52
- console.log();
636
+ const name = s.session_name
637
+ ? chalk_1.default.hex("#89b4fa")(s.session_name)
638
+ : chalk_1.default.dim("(unnamed)");
639
+ // Session ID hidden — not useful to non-technical users
640
+ const turns = chalk_1.default.dim(`${s.message_count} turns`);
641
+ const cost = chalk_1.default.dim(`$${Number(s.total_cost_usd).toFixed(4)}`);
642
+ const lastActive = chalk_1.default.dim(new Date(s.last_active_at).toLocaleString());
643
+ const status = s.status === "active"
644
+ ? chalk_1.default.hex("#a6e3a1")("●")
645
+ : chalk_1.default.dim("○");
646
+ process.stdout.write(` ${status} ${name}\n`);
647
+ process.stdout.write(` ${turns} ${cost} ${lastActive}\n\n`);
53
648
  }
54
- console.log(chalk_1.default.dim("Resume: xiaotime dev --session <id>"));
649
+ process.stdout.write(chalk_1.default.dim("Resume: xiaotime dev --session <id>\n\n"));
55
650
  }
56
- function printPrompt() {
57
- process.stdout.write(chalk_1.default.bold("you: "));
651
+ // ---------------------------------------------------------------------------
652
+ // Tool call indicators
653
+ // ---------------------------------------------------------------------------
654
+ const TOOL_ICONS = {
655
+ read_file: "▸",
656
+ write_file: "▸",
657
+ list_directory: "▸",
658
+ shell_command: "▸",
659
+ git_status: "▸",
660
+ git_diff: "▸",
661
+ git_commit: "▸",
662
+ };
663
+ const SERVER_TOOL_ICONS = {
664
+ web_search: "▸",
665
+ lookup_entity: "▸",
666
+ remember_entity: "▸",
667
+ signal_query: "▸",
668
+ research_request: "▸",
669
+ note_ingest: "▸",
670
+ check_calendar: "▸",
671
+ meeting_prep: "▸",
672
+ list_entities: "▸",
673
+ };
674
+ // ---------------------------------------------------------------------------
675
+ // Human-friendly tool status labels for non-developer users
676
+ // ---------------------------------------------------------------------------
677
+ const TOOL_FRIENDLY_NAMES = {
678
+ web_search: "searching the web...",
679
+ lookup_entity: "looking that up...",
680
+ remember_entity: "saving that...",
681
+ signal_query: "checking signals...",
682
+ research_request: "doing some research...",
683
+ note_ingest: "saving your note...",
684
+ check_calendar: "checking your calendar...",
685
+ meeting_prep: "preparing your meeting brief...",
686
+ list_entities: "pulling up your contacts...",
687
+ list_pipeline_accounts: "checking your pipeline...",
688
+ entity_history: "reviewing history...",
689
+ account_deep_view: "digging into that account...",
690
+ stale_contacts: "finding stale contacts...",
691
+ resolve_person: "looking up that person...",
692
+ link_entities: "making that connection...",
693
+ get_connections: "finding connections...",
694
+ precall_brief: "prepping your brief...",
695
+ objection_prep: "preparing objection handling...",
696
+ competitive_intel: "pulling competitive intel...",
697
+ habit_log: "logging that habit...",
698
+ habit_streak: "checking your streaks...",
699
+ schedule_job: "scheduling that...",
700
+ list_scheduled_jobs: "loading scheduled jobs...",
701
+ cancel_scheduled_job: "cancelling that job...",
702
+ log_decision: "recording that decision...",
703
+ signal_stats: "pulling signal stats...",
704
+ read_file: "reading file...",
705
+ write_file: "writing file...",
706
+ list_directory: "listing files...",
707
+ shell_command: "running command...",
708
+ git_status: "checking git status...",
709
+ git_diff: "reading changes...",
710
+ git_commit: "committing...",
711
+ generate_image: "generating image...",
712
+ run_background_task: "starting background task...",
713
+ summarize_url: "reading that page...",
714
+ fetch_url: "fetching that page...",
715
+ };
716
+ function friendlyToolLabel(name) {
717
+ return TOOL_FRIENDLY_NAMES[name] ?? `working on it...`;
58
718
  }
59
- /**
60
- * Render a single inline log line for a server-side tool invocation.
61
- *
62
- * SPRINT-CLI-APPROVAL-MENU-1 (#6509) PR3 — pre-#6509 the 15 tools in
63
- * terminal_orchestrator/main.py's SERVER_TOOLS set executed silently
64
- * from the user's perspective. This helper renders the
65
- * `server_tool_call` WS event the orchestrator now emits before each
66
- * server-side dispatch, matching the visual style of the existing
67
- * client-tool inline logs in cli/src/tools.ts (`[read_file] /path
68
- * (N lines)`, `[git_status]`, etc.).
69
- *
70
- * `inputSummary` is built server-side (see
71
- * `terminal_orchestrator/main.py::_build_server_tool_input_summary`)
72
- * — ≤120 chars, preferred-keys-per-tool, middle-truncated values,
73
- * no newlines. Empty inputSummary (e.g. librarian_list_collections,
74
- * which has no meaningful input to render) drops to just the
75
- * `[server:<name>]` prefix.
76
- */
77
- function printServerToolCall(name, inputSummary) {
78
- const tail = inputSummary ? ` ${inputSummary}` : "";
79
- console.log(chalk_1.default.dim(` [server:${name}]${tail}`));
719
+ // ---------------------------------------------------------------------------
720
+ // Friendly plain-English error messages
721
+ // ---------------------------------------------------------------------------
722
+ const ERROR_PATTERNS = [
723
+ [/ECONNREFUSED/i, "Can't reach the server. Check your connection and try again."],
724
+ [/ETIMEDOUT|TIMEOUT/i, "The request timed out. The server might be busy — try again in a moment."],
725
+ [/ENOTFOUND/i, "Couldn't connect check your internet connection."],
726
+ [/401|unauthorized/i, "Not signed in. Run `xiaotime login` to authenticate."],
727
+ [/403|forbidden/i, "You don't have permission to do that."],
728
+ [/404|not found/i, "That resource wasn't found."],
729
+ [/500|internal server/i, "Something went wrong on the server. Try again shortly."],
730
+ [/EPIPE/i, "Connection was interrupted. Reconnect and try again."],
731
+ [/WEBSOCKET|ws error/i, "Lost the live connection. Reconnecting..."],
732
+ ];
733
+ function friendlyError(raw) {
734
+ for (const [pattern, friendly] of ERROR_PATTERNS) {
735
+ if (pattern.test(raw))
736
+ return friendly;
737
+ }
738
+ // Fall back strip stack trace noise, just show first line
739
+ const firstLine = raw.split("\n")[0].replace(/^Error:\s*/i, "").trim();
740
+ return firstLine.length > 0 && firstLine.length < 200 ? firstLine : "Something went wrong. Try again.";
741
+ }
742
+ // ---------------------------------------------------------------------------
743
+ // Help panel
744
+ // ---------------------------------------------------------------------------
745
+ function printHelp() {
746
+ const w = termWidth();
747
+ const top = chalk_1.default.hex("#f9e2af")("╭") +
748
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
749
+ chalk_1.default.hex("#f9e2af")("╮");
750
+ const bottom = chalk_1.default.hex("#f9e2af")("╰") +
751
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
752
+ chalk_1.default.hex("#f9e2af")("╯");
753
+ const lines = [
754
+ chalk_1.default.bold.hex("#f9e2af")(" Junior — your AI chief of staff"),
755
+ "",
756
+ chalk_1.default.dim(" Just type naturally. Some examples:"),
757
+ "",
758
+ ` ${chalk_1.default.hex("#89b4fa")("what's on my calendar today")}`,
759
+ ` ${chalk_1.default.hex("#89b4fa")("draft a follow-up email to Acme")}`,
760
+ ` ${chalk_1.default.hex("#89b4fa")("look up Banner Health")}`,
761
+ ` ${chalk_1.default.hex("#89b4fa")("what signals do I have this week")}`,
762
+ ` ${chalk_1.default.hex("#89b4fa")("research CrowdStrike")}`,
763
+ "",
764
+ chalk_1.default.dim(" Keyboard shortcuts:"),
765
+ ` ${chalk_1.default.dim("ctrl+c")} exit ${chalk_1.default.dim("ctrl+r")} switch sessions`,
766
+ ` ${chalk_1.default.dim("ctrl+l")} clear screen ${chalk_1.default.dim("ctrl+u")} scroll history`,
767
+ ` ${chalk_1.default.dim("↑ ↓")} message history`,
768
+ "",
769
+ chalk_1.default.dim(" Type anything to get started. Junior will figure out the rest."),
770
+ ];
771
+ process.stdout.write("\n" + top + "\n");
772
+ for (const l of lines) {
773
+ process.stdout.write(l + "\n");
774
+ }
775
+ process.stdout.write(bottom + "\n\n");
776
+ }
777
+ // ---------------------------------------------------------------------------
778
+ // Disconnect banner
779
+ // ---------------------------------------------------------------------------
780
+ function printDisconnectBanner(reconnecting = true) {
781
+ const msg = reconnecting
782
+ ? " Lost connection — trying to reconnect..."
783
+ : " Disconnected. Resume with: xiaotime dev";
784
+ process.stdout.write("\n" + chalk_1.default.dim(msg) + "\n");
785
+ }
786
+ // ---------------------------------------------------------------------------
787
+ // Exit confirmation
788
+ // ---------------------------------------------------------------------------
789
+ function printExitConfirmation() {
790
+ process.stdout.write("\n" + chalk_1.default.hex("#f9e2af")(" Session paused. Your conversation is saved.") + "\n");
791
+ process.stdout.write(chalk_1.default.dim(" Resume anytime with: xiaotime dev") + "\n\n");
792
+ }
793
+ function printToolCallLine(name, detail, durationMs, success = true) {
794
+ setLastTool(name);
795
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
796
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
797
+ const detailStr = detail ? chalk_1.default.dim(` ${detail}`) : "";
798
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(name)}${detailStr}${dur}\n`);
799
+ }
800
+ function printServerToolCall(name, inputSummary, durationMs) {
801
+ setLastTool(name);
802
+ const icon = chalk_1.default.hex("#a6e3a1")("✓");
803
+ const tail = inputSummary ? chalk_1.default.dim(` ${inputSummary}`) : "";
804
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
805
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(name)}${tail}${dur}\n`);
80
806
  }
81
807
  //# sourceMappingURL=display.js.map