xiaotime 0.5.1 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +750 -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 +226 -213
  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 +240 -119
  49. package/dist/ws.js.map +1 -1
  50. package/package.json +10 -5
package/dist/display.js CHANGED
@@ -1,81 +1,784 @@
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
+ // The persistent bottom status bar is intentionally NOT drawn in the terminal.
213
+ //
214
+ // A truly sticky bottom bar requires a DECSTBM scroll region. That approach
215
+ // (shipped briefly in 0.5.3) forced the input box to the bottom of the screen
216
+ // with a large empty gap on real SSH clients (Termius); without the region the
217
+ // absolute-positioned bar leaks a copy into scrollback on every redraw — the
218
+ // "Xiaotime Labs repeating up the window" symptom. Neither is acceptable, and
219
+ // terminal-control iteration can't be verified headless.
220
+ //
221
+ // Session context already appears in the welcome header. The proper live status
222
+ // bar belongs in the portal terminal, where it is a real pinned DOM element.
223
+ // State is still tracked below so a future renderer (or the portal) can use it.
224
+ function _drawStickyBar() {
225
+ // no-op — see note above. Status state is tracked; nothing is painted.
226
+ }
227
+ function initStatusBar(sessionId, sessionName, tenant) {
228
+ _statusBar.sessionId = sessionId;
229
+ _statusBar.sessionName = sessionName;
230
+ if (tenant)
231
+ _statusBar.tenant = tenant;
232
+ _statusBar.connected = false;
233
+ _drawStickyBar();
234
+ }
235
+ function setConnected(connected) {
236
+ _statusBar.connected = connected;
237
+ _drawStickyBar();
238
+ }
239
+ function incrementTurn() {
240
+ _statusBar.turnCount += 1;
241
+ _drawStickyBar();
242
+ }
243
+ function setLastTool(toolName) {
244
+ _statusBar.lastTool = toolName;
245
+ _drawStickyBar();
246
+ }
247
+ function updateStatusBarCost(_turnCostUsd) {
248
+ // no-op — cost not shown in bar
249
+ }
250
+ // ---------------------------------------------------------------------------
251
+ // Thinking spinner
252
+ // ---------------------------------------------------------------------------
253
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
254
+ let _spinnerInterval = null;
255
+ let _spinnerFrame = 0;
256
+ let _spinnerLabel = "";
257
+ let _spinnerActive = false;
258
+ function _clearSpinnerLine() {
259
+ process.stdout.write("\x1b[2K\r");
260
+ }
261
+ function startSpinner(label) {
262
+ if (_spinnerActive)
263
+ stopSpinner();
264
+ _spinnerLabel = label;
265
+ _spinnerActive = true;
266
+ _spinnerFrame = 0;
267
+ _spinnerInterval = setInterval(() => {
268
+ if (!_spinnerActive)
269
+ return;
270
+ const frame = chalk_1.default.hex("#c0384b")(SPINNER_FRAMES[_spinnerFrame % SPINNER_FRAMES.length]);
271
+ _spinnerFrame++;
272
+ const labelPart = _spinnerLabel ? ` ${chalk_1.default.dim(_spinnerLabel)}` : "";
273
+ process.stdout.write(`\r ${frame}${labelPart}`);
274
+ }, 80);
275
+ }
276
+ function stopSpinner(finalLabel, success = true) {
277
+ if (_spinnerInterval) {
278
+ clearInterval(_spinnerInterval);
279
+ _spinnerInterval = null;
280
+ }
281
+ _spinnerActive = false;
282
+ _clearSpinnerLine();
283
+ if (finalLabel) {
284
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
285
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(finalLabel)}\n`);
286
+ }
287
+ }
288
+ let _activeTool = null;
289
+ function startToolLine(name, detail) {
290
+ if (_activeTool)
291
+ finishToolLine(true);
292
+ setLastTool(name);
293
+ let frame = 0;
294
+ const spinnerInterval = setInterval(() => {
295
+ const f = chalk_1.default.hex("#89b4fa")(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
296
+ const friendly = friendlyToolLabel(name);
297
+ const detailStr = detail ? chalk_1.default.dim(` ${detail}`) : "";
298
+ process.stdout.write(`\r ${f} ${chalk_1.default.dim(friendly)}${detailStr}`);
299
+ frame++;
300
+ }, 80);
301
+ _activeTool = {
302
+ name,
303
+ detail,
304
+ startedAt: Date.now(),
305
+ spinnerInterval,
306
+ frame: 0,
307
+ };
308
+ }
309
+ function finishToolLine(success = true, errorMessage) {
310
+ if (!_activeTool)
311
+ return;
312
+ clearInterval(_activeTool.spinnerInterval);
313
+ const elapsed = Date.now() - _activeTool.startedAt;
314
+ if (!success && errorMessage) {
315
+ // Clear the spinner line, then render the red error box
316
+ process.stdout.write(`\r\x1b[2K`);
317
+ printToolError(_activeTool.name, errorMessage, elapsed);
21
318
  }
22
319
  else {
23
- console.log(chalk_1.default.cyan(`\n✦ New session: ${name}`));
320
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
321
+ const detailStr = _activeTool.detail ? chalk_1.default.dim(` ${_activeTool.detail}`) : "";
322
+ const dur = chalk_1.default.dim(` ${elapsed}ms`);
323
+ const friendlyLabel = friendlyToolLabel(_activeTool.name);
324
+ process.stdout.write(`\r\x1b[2K ${icon} ${chalk_1.default.dim(friendlyLabel)}${detailStr}${dur}\n`);
325
+ }
326
+ _activeTool = null;
327
+ }
328
+ // ---------------------------------------------------------------------------
329
+ // Tool error — red bordered box
330
+ // ---------------------------------------------------------------------------
331
+ function printToolError(toolName, errorMessage, durationMs) {
332
+ const w = termWidth();
333
+ const top = chalk_1.default.hex("#f38ba8")("╭") +
334
+ chalk_1.default.hex("#f38ba8")("─".repeat(w - 2)) +
335
+ chalk_1.default.hex("#f38ba8")("╮");
336
+ const bottom = chalk_1.default.hex("#f38ba8")("╰") +
337
+ chalk_1.default.hex("#f38ba8")("─".repeat(w - 2)) +
338
+ chalk_1.default.hex("#f38ba8")("╯");
339
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
340
+ const titleLine = chalk_1.default.bold.hex("#f38ba8")(` ✗ ${toolName}${dur}`);
341
+ const bodyLines = wordWrap(errorMessage, w - 4).map((l) => chalk_1.default.hex("#f38ba8")(" " + l));
342
+ process.stdout.write("\n" + top + "\n");
343
+ process.stdout.write(titleLine + "\n");
344
+ for (const l of bodyLines) {
345
+ process.stdout.write(l + "\n");
24
346
  }
25
- console.log(chalk_1.default.dim("Type your message. Ctrl+C to exit.\n"));
347
+ process.stdout.write(bottom + "\n\n");
26
348
  }
349
+ // ---------------------------------------------------------------------------
350
+ // Diff view for write_file
351
+ // ---------------------------------------------------------------------------
352
+ function printFileDiff(path, before, after) {
353
+ const w = termWidth();
354
+ const label = chalk_1.default.dim(` write_file ${path} `);
355
+ const labelRaw = ` write_file ${path} `;
356
+ const rule = chalk_1.default.dim("─".repeat(Math.max(0, w - labelRaw.length)));
357
+ process.stdout.write("\n" + rule + label + "\n");
358
+ if (!before) {
359
+ // New file — show all lines as added
360
+ const lines = after.split("\n").slice(0, 20);
361
+ for (const line of lines) {
362
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + line) + "\n");
363
+ }
364
+ if (after.split("\n").length > 20) {
365
+ process.stdout.write(chalk_1.default.dim(` … ${after.split("\n").length - 20} more lines\n`));
366
+ }
367
+ }
368
+ else {
369
+ const beforeLines = before.split("\n");
370
+ const afterLines = after.split("\n");
371
+ let shown = 0;
372
+ const maxLines = 30;
373
+ for (let i = 0; i < Math.max(beforeLines.length, afterLines.length) && shown < maxLines; i++) {
374
+ const b = beforeLines[i];
375
+ const a = afterLines[i];
376
+ if (b === undefined) {
377
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + a) + "\n");
378
+ shown++;
379
+ }
380
+ else if (a === undefined) {
381
+ process.stdout.write(chalk_1.default.hex("#f38ba8")("- " + b) + "\n");
382
+ shown++;
383
+ }
384
+ else if (b !== a) {
385
+ process.stdout.write(chalk_1.default.hex("#f38ba8")("- " + b) + "\n");
386
+ process.stdout.write(chalk_1.default.hex("#a6e3a1")("+ " + a) + "\n");
387
+ shown += 2;
388
+ }
389
+ }
390
+ const totalDiff = Math.abs(afterLines.length - beforeLines.length);
391
+ if (totalDiff > maxLines) {
392
+ process.stdout.write(chalk_1.default.dim(` … ${totalDiff - maxLines} more changes\n`));
393
+ }
394
+ }
395
+ process.stdout.write(chalk_1.default.dim("─".repeat(w)) + "\n\n");
396
+ }
397
+ // ---------------------------------------------------------------------------
398
+ // Approval prompt — yellow bordered box
399
+ // ---------------------------------------------------------------------------
400
+ function printApprovalPrompt(title, body) {
401
+ const w = termWidth();
402
+ const top = chalk_1.default.hex("#f9e2af")("╭") +
403
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
404
+ chalk_1.default.hex("#f9e2af")("╮");
405
+ const bottom = chalk_1.default.hex("#f9e2af")("╰") +
406
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
407
+ chalk_1.default.hex("#f9e2af")("╯");
408
+ const titleLine = chalk_1.default.bold.hex("#f9e2af")(" " + title);
409
+ const bodyLines = wordWrap(body, w - 4).map((l) => chalk_1.default.hex("#f9e2af")(" " + l));
410
+ process.stdout.write("\n" + top + "\n");
411
+ process.stdout.write(titleLine + "\n");
412
+ for (const l of bodyLines) {
413
+ process.stdout.write(l + "\n");
414
+ }
415
+ process.stdout.write(bottom + "\n");
416
+ }
417
+ // ---------------------------------------------------------------------------
418
+ // Streaming buffer — handle code fences in real-time, markdown on flush
419
+ // ---------------------------------------------------------------------------
420
+ let _streamBuf = "";
421
+ let _inCodeFence = false;
422
+ let _codeFenceLang = "";
423
+ let _codeFenceBuffer = [];
424
+ let _turnHasContent = false;
27
425
  function printTextDelta(text) {
28
- process.stdout.write(text);
426
+ // Stop spinner on first token
427
+ if (_spinnerActive)
428
+ stopSpinner();
429
+ if (_activeTool)
430
+ finishToolLine(true);
431
+ _streamBuf += text;
432
+ const lines = _streamBuf.split("\n");
433
+ const complete = lines.slice(0, -1);
434
+ _streamBuf = lines[lines.length - 1];
435
+ for (const line of complete) {
436
+ _writeLine(line);
437
+ }
438
+ }
439
+ function _writeLine(line) {
440
+ _turnHasContent = true;
441
+ if (!_inCodeFence) {
442
+ const fenceMatch = line.match(/^```(\w*)$/);
443
+ if (fenceMatch) {
444
+ _inCodeFence = true;
445
+ _codeFenceLang = fenceMatch[1] || "plaintext";
446
+ _codeFenceBuffer = [];
447
+ const w = termWidth();
448
+ const lang = _codeFenceLang !== "plaintext" ? _codeFenceLang : "";
449
+ const label = lang ? chalk_1.default.dim(` ${lang} `) : "";
450
+ const labelRaw = lang ? ` ${lang} ` : "";
451
+ const rule = chalk_1.default.dim("─".repeat(Math.max(0, w - labelRaw.length)));
452
+ process.stdout.write(rule + label + "\n");
453
+ return;
454
+ }
455
+ const w = termWidth();
456
+ const rendered = renderMarkdownLine(line);
457
+ const rawLen = stripAnsi(rendered).length;
458
+ if (rawLen <= w) {
459
+ process.stdout.write(rendered + "\n");
460
+ }
461
+ else {
462
+ const segments = wordWrap(line, w);
463
+ for (const seg of segments) {
464
+ process.stdout.write(renderMarkdownLine(seg) + "\n");
465
+ }
466
+ }
467
+ }
468
+ else {
469
+ if (line === "```") {
470
+ const code = _codeFenceBuffer.join("\n");
471
+ const language = _codeFenceLang && (0, cli_highlight_1.supportsLanguage)(_codeFenceLang)
472
+ ? _codeFenceLang
473
+ : "plaintext";
474
+ try {
475
+ process.stdout.write((0, cli_highlight_1.highlight)(code, { language, ignoreIllegals: true }) + "\n");
476
+ }
477
+ catch {
478
+ process.stdout.write(chalk_1.default.dim(code) + "\n");
479
+ }
480
+ process.stdout.write(chalk_1.default.dim("─".repeat(termWidth())) + "\n");
481
+ _inCodeFence = false;
482
+ _codeFenceLang = "";
483
+ _codeFenceBuffer = [];
484
+ }
485
+ else {
486
+ _codeFenceBuffer.push(line);
487
+ }
488
+ }
489
+ }
490
+ // ---------------------------------------------------------------------------
491
+ // Turn lifecycle
492
+ // ---------------------------------------------------------------------------
493
+ function printTurnStart() {
494
+ process.stdout.write("\x1b[?25l"); // hide cursor
495
+ process.stdout.write("\n" +
496
+ chalk_1.default.hex("#c0384b")("Junior") +
497
+ chalk_1.default.dim(" › ") +
498
+ "\n");
499
+ _turnHasContent = false;
500
+ }
501
+ function printThinkingStart() {
502
+ startSpinner("");
503
+ }
504
+ function printThinkingStop() {
505
+ stopSpinner();
29
506
  }
30
507
  function printTurnEnd(costUsd) {
31
- const costStr = costUsd > 0 ? chalk_1.default.dim(` [$${costUsd.toFixed(4)}]`) : "";
32
- process.stdout.write(`\n${costStr}\n\n`);
508
+ process.stdout.write("\x1b[?25h"); // restore cursor
509
+ // Stop any lingering spinner or tool line
510
+ if (_spinnerActive)
511
+ stopSpinner();
512
+ if (_activeTool)
513
+ finishToolLine(true);
514
+ if (_streamBuf.length > 0) {
515
+ _writeLine(_streamBuf);
516
+ _streamBuf = "";
517
+ }
518
+ _inCodeFence = false;
519
+ _codeFenceBuffer = [];
520
+ incrementTurn();
521
+ process.stdout.write("\n" + chalk_1.default.dim("─".repeat(termWidth())) + "\n");
522
+ }
523
+ // ---------------------------------------------------------------------------
524
+ // Input prompt — top border only, no repeated shortcut bar
525
+ // ---------------------------------------------------------------------------
526
+ // ---------------------------------------------------------------------------
527
+ // First-launch state — show placeholder hint on very first prompt
528
+ // ---------------------------------------------------------------------------
529
+ let _isFirstPrompt = true;
530
+ function consumeFirstPrompt() {
531
+ const was = _isFirstPrompt;
532
+ _isFirstPrompt = false;
533
+ return was;
534
+ }
535
+ function printPrompt(_showHint = false) {
536
+ const w = termWidth();
537
+ const topBorder = chalk_1.default.dim("╭") +
538
+ chalk_1.default.dim("─".repeat(w - 2)) +
539
+ chalk_1.default.dim("╮");
540
+ // No inline placeholder hint. readline echoes typed input immediately after
541
+ // the "you › " prefix, so a static hint here does not clear on focus/typing —
542
+ // it just gets text appended to it ("…exampleshey junior"). The "type a
543
+ // message, or 'help' for examples" guidance lives in the welcome header
544
+ // instead, where nothing is typed over it.
545
+ process.stdout.write("\n" +
546
+ topBorder + "\n" +
547
+ chalk_1.default.bold.hex("#c0384b")("you") +
548
+ chalk_1.default.dim(" › "));
549
+ }
550
+ function printPromptEnd() {
551
+ const w = termWidth();
552
+ const bottomBorder = chalk_1.default.dim("╰") +
553
+ chalk_1.default.dim("─".repeat(w - 2)) +
554
+ chalk_1.default.dim("╯");
555
+ process.stdout.write("\n" + bottomBorder + "\n");
556
+ }
557
+ // ---------------------------------------------------------------------------
558
+ // Welcome + session list
559
+ // ---------------------------------------------------------------------------
560
+ function printWelcome(sessionId, sessionName, resumed, messageCount, lastMessagePreview) {
561
+ initStatusBar(sessionId, sessionName);
562
+ const name = sessionName || sessionId.slice(0, 8);
563
+ // Shortcut bar — shown ONCE on welcome only
564
+ const w = termWidth();
565
+ const shortcuts = [
566
+ chalk_1.default.dim("ctrl+c") + " " + chalk_1.default.hex("#6c7086")("exit"),
567
+ chalk_1.default.dim("ctrl+r") + " " + chalk_1.default.hex("#6c7086")("sessions"),
568
+ chalk_1.default.dim("ctrl+l") + " " + chalk_1.default.hex("#6c7086")("clear"),
569
+ chalk_1.default.dim("↑↓") + " " + chalk_1.default.hex("#6c7086")("history"),
570
+ ].join(chalk_1.default.dim(" · "));
571
+ const shortcutRaw = stripAnsi(shortcuts);
572
+ const shortcutPad = Math.max(0, Math.floor((w - shortcutRaw.length) / 2));
573
+ if (resumed) {
574
+ process.stdout.write("\n" +
575
+ chalk_1.default.hex("#89b4fa")("↩ " + name) +
576
+ chalk_1.default.dim(` ${messageCount} turns`) +
577
+ "\n");
578
+ if (lastMessagePreview) {
579
+ const truncated = lastMessagePreview.length > 60
580
+ ? lastMessagePreview.slice(0, 60) + "…"
581
+ : lastMessagePreview;
582
+ process.stdout.write(chalk_1.default.dim(' last: "' + truncated + '"') + "\n");
583
+ }
584
+ process.stdout.write("\n" +
585
+ " ".repeat(shortcutPad) + shortcuts + "\n\n");
586
+ }
587
+ else {
588
+ process.stdout.write("\n" +
589
+ chalk_1.default.hex("#c0384b").bold("✦ Junior") +
590
+ chalk_1.default.dim(` ${name}`) +
591
+ "\n\n" +
592
+ " ".repeat(shortcutPad) + shortcuts + "\n\n");
593
+ }
33
594
  }
34
595
  function printError(message) {
35
- console.error(chalk_1.default.red(`\nError: ${message}\n`));
596
+ process.stderr.write(chalk_1.default.red("\n✗ ") +
597
+ chalk_1.default.bold.red("Error: ") +
598
+ chalk_1.default.red(message) +
599
+ "\n\n");
36
600
  }
37
601
  function printSessionList(sessions) {
38
602
  if (sessions.length === 0) {
39
603
  console.log(chalk_1.default.dim("No sessions found."));
40
604
  return;
41
605
  }
42
- console.log(chalk_1.default.bold("\nSessions:\n"));
606
+ const w = termWidth();
607
+ process.stdout.write("\n" +
608
+ chalk_1.default.bold.hex("#c0384b")("Sessions") +
609
+ "\n" +
610
+ chalk_1.default.dim("─".repeat(w)) +
611
+ "\n");
43
612
  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();
613
+ const name = s.session_name
614
+ ? chalk_1.default.hex("#89b4fa")(s.session_name)
615
+ : chalk_1.default.dim("(unnamed)");
616
+ // Session ID hidden — not useful to non-technical users
617
+ const turns = chalk_1.default.dim(`${s.message_count} turns`);
618
+ const cost = chalk_1.default.dim(`$${Number(s.total_cost_usd).toFixed(4)}`);
619
+ const lastActive = chalk_1.default.dim(new Date(s.last_active_at).toLocaleString());
620
+ const status = s.status === "active"
621
+ ? chalk_1.default.hex("#a6e3a1")("●")
622
+ : chalk_1.default.dim("○");
623
+ process.stdout.write(` ${status} ${name}\n`);
624
+ process.stdout.write(` ${turns} ${cost} ${lastActive}\n\n`);
53
625
  }
54
- console.log(chalk_1.default.dim("Resume: xiaotime dev --session <id>"));
626
+ process.stdout.write(chalk_1.default.dim("Resume: xiaotime dev --session <id>\n\n"));
55
627
  }
56
- function printPrompt() {
57
- process.stdout.write(chalk_1.default.bold("you: "));
628
+ // ---------------------------------------------------------------------------
629
+ // Tool call indicators
630
+ // ---------------------------------------------------------------------------
631
+ const TOOL_ICONS = {
632
+ read_file: "▸",
633
+ write_file: "▸",
634
+ list_directory: "▸",
635
+ shell_command: "▸",
636
+ git_status: "▸",
637
+ git_diff: "▸",
638
+ git_commit: "▸",
639
+ };
640
+ const SERVER_TOOL_ICONS = {
641
+ web_search: "▸",
642
+ lookup_entity: "▸",
643
+ remember_entity: "▸",
644
+ signal_query: "▸",
645
+ research_request: "▸",
646
+ note_ingest: "▸",
647
+ check_calendar: "▸",
648
+ meeting_prep: "▸",
649
+ list_entities: "▸",
650
+ };
651
+ // ---------------------------------------------------------------------------
652
+ // Human-friendly tool status labels for non-developer users
653
+ // ---------------------------------------------------------------------------
654
+ const TOOL_FRIENDLY_NAMES = {
655
+ web_search: "searching the web...",
656
+ lookup_entity: "looking that up...",
657
+ remember_entity: "saving that...",
658
+ signal_query: "checking signals...",
659
+ research_request: "doing some research...",
660
+ note_ingest: "saving your note...",
661
+ check_calendar: "checking your calendar...",
662
+ meeting_prep: "preparing your meeting brief...",
663
+ list_entities: "pulling up your contacts...",
664
+ list_pipeline_accounts: "checking your pipeline...",
665
+ entity_history: "reviewing history...",
666
+ account_deep_view: "digging into that account...",
667
+ stale_contacts: "finding stale contacts...",
668
+ resolve_person: "looking up that person...",
669
+ link_entities: "making that connection...",
670
+ get_connections: "finding connections...",
671
+ precall_brief: "prepping your brief...",
672
+ objection_prep: "preparing objection handling...",
673
+ competitive_intel: "pulling competitive intel...",
674
+ habit_log: "logging that habit...",
675
+ habit_streak: "checking your streaks...",
676
+ schedule_job: "scheduling that...",
677
+ list_scheduled_jobs: "loading scheduled jobs...",
678
+ cancel_scheduled_job: "cancelling that job...",
679
+ log_decision: "recording that decision...",
680
+ signal_stats: "pulling signal stats...",
681
+ read_file: "reading file...",
682
+ write_file: "writing file...",
683
+ list_directory: "listing files...",
684
+ shell_command: "running command...",
685
+ git_status: "checking git status...",
686
+ git_diff: "reading changes...",
687
+ git_commit: "committing...",
688
+ generate_image: "generating image...",
689
+ run_background_task: "starting background task...",
690
+ summarize_url: "reading that page...",
691
+ fetch_url: "fetching that page...",
692
+ };
693
+ function friendlyToolLabel(name) {
694
+ return TOOL_FRIENDLY_NAMES[name] ?? `working on it...`;
58
695
  }
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}`));
696
+ // ---------------------------------------------------------------------------
697
+ // Friendly plain-English error messages
698
+ // ---------------------------------------------------------------------------
699
+ const ERROR_PATTERNS = [
700
+ [/ECONNREFUSED/i, "Can't reach the server. Check your connection and try again."],
701
+ [/ETIMEDOUT|TIMEOUT/i, "The request timed out. The server might be busy — try again in a moment."],
702
+ [/ENOTFOUND/i, "Couldn't connect check your internet connection."],
703
+ [/401|unauthorized/i, "Not signed in. Run `xiaotime login` to authenticate."],
704
+ [/403|forbidden/i, "You don't have permission to do that."],
705
+ [/404|not found/i, "That resource wasn't found."],
706
+ [/500|internal server/i, "Something went wrong on the server. Try again shortly."],
707
+ [/EPIPE/i, "Connection was interrupted. Reconnect and try again."],
708
+ [/WEBSOCKET|ws error/i, "Lost the live connection. Reconnecting..."],
709
+ ];
710
+ function friendlyError(raw) {
711
+ for (const [pattern, friendly] of ERROR_PATTERNS) {
712
+ if (pattern.test(raw))
713
+ return friendly;
714
+ }
715
+ // Fall back strip stack trace noise, just show first line
716
+ const firstLine = raw.split("\n")[0].replace(/^Error:\s*/i, "").trim();
717
+ return firstLine.length > 0 && firstLine.length < 200 ? firstLine : "Something went wrong. Try again.";
718
+ }
719
+ // ---------------------------------------------------------------------------
720
+ // Help panel
721
+ // ---------------------------------------------------------------------------
722
+ function printHelp() {
723
+ const w = termWidth();
724
+ const top = chalk_1.default.hex("#f9e2af")("╭") +
725
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
726
+ chalk_1.default.hex("#f9e2af")("╮");
727
+ const bottom = chalk_1.default.hex("#f9e2af")("╰") +
728
+ chalk_1.default.hex("#f9e2af")("─".repeat(w - 2)) +
729
+ chalk_1.default.hex("#f9e2af")("╯");
730
+ const lines = [
731
+ chalk_1.default.bold.hex("#f9e2af")(" Junior — your AI chief of staff"),
732
+ "",
733
+ chalk_1.default.dim(" Just type naturally. Some examples:"),
734
+ "",
735
+ ` ${chalk_1.default.hex("#89b4fa")("what's on my calendar today")}`,
736
+ ` ${chalk_1.default.hex("#89b4fa")("draft a follow-up email to Acme")}`,
737
+ ` ${chalk_1.default.hex("#89b4fa")("look up Banner Health")}`,
738
+ ` ${chalk_1.default.hex("#89b4fa")("what signals do I have this week")}`,
739
+ ` ${chalk_1.default.hex("#89b4fa")("research CrowdStrike")}`,
740
+ "",
741
+ chalk_1.default.dim(" Keyboard shortcuts:"),
742
+ ` ${chalk_1.default.dim("ctrl+c")} exit ${chalk_1.default.dim("ctrl+r")} switch sessions`,
743
+ ` ${chalk_1.default.dim("ctrl+l")} clear screen ${chalk_1.default.dim("ctrl+u")} scroll history`,
744
+ ` ${chalk_1.default.dim("↑ ↓")} message history`,
745
+ "",
746
+ chalk_1.default.dim(" Type anything to get started. Junior will figure out the rest."),
747
+ ];
748
+ process.stdout.write("\n" + top + "\n");
749
+ for (const l of lines) {
750
+ process.stdout.write(l + "\n");
751
+ }
752
+ process.stdout.write(bottom + "\n\n");
753
+ }
754
+ // ---------------------------------------------------------------------------
755
+ // Disconnect banner
756
+ // ---------------------------------------------------------------------------
757
+ function printDisconnectBanner(reconnecting = true) {
758
+ const msg = reconnecting
759
+ ? " Lost connection — trying to reconnect..."
760
+ : " Disconnected. Resume with: xiaotime dev";
761
+ process.stdout.write("\n" + chalk_1.default.dim(msg) + "\n");
762
+ }
763
+ // ---------------------------------------------------------------------------
764
+ // Exit confirmation
765
+ // ---------------------------------------------------------------------------
766
+ function printExitConfirmation() {
767
+ process.stdout.write("\n" + chalk_1.default.hex("#f9e2af")(" Session paused. Your conversation is saved.") + "\n");
768
+ process.stdout.write(chalk_1.default.dim(" Resume anytime with: xiaotime dev") + "\n\n");
769
+ }
770
+ function printToolCallLine(name, detail, durationMs, success = true) {
771
+ setLastTool(name);
772
+ const icon = success ? chalk_1.default.hex("#a6e3a1")("✓") : chalk_1.default.hex("#f38ba8")("✗");
773
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
774
+ const detailStr = detail ? chalk_1.default.dim(` ${detail}`) : "";
775
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(name)}${detailStr}${dur}\n`);
776
+ }
777
+ function printServerToolCall(name, inputSummary, durationMs) {
778
+ setLastTool(name);
779
+ const icon = chalk_1.default.hex("#a6e3a1")("✓");
780
+ const tail = inputSummary ? chalk_1.default.dim(` ${inputSummary}`) : "";
781
+ const dur = durationMs !== undefined ? chalk_1.default.dim(` ${durationMs}ms`) : "";
782
+ process.stdout.write(` ${icon} ${chalk_1.default.dim(name)}${tail}${dur}\n`);
80
783
  }
81
784
  //# sourceMappingURL=display.js.map