@stevezhou/sisu 0.1.8 → 0.1.10

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/logo.js CHANGED
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "sisuTreeArt", { enumerable: true, get: function
18
18
  Object.defineProperty(exports, "sisuTreeLines", { enumerable: true, get: function () { return tree_1.sisuTreeLines; } });
19
19
  const mark_2 = require("./mark");
20
20
  const mobius_1 = require("./mobius");
21
+ const theme_1 = require("./pager/theme");
21
22
  const RESET = '\x1b[0m';
22
23
  const INK = '\x1b[38;2;220;214;204m';
23
24
  const INK_DIM = '\x1b[38;2;140;132;120m';
@@ -33,24 +34,8 @@ function sisuMobiusArt(columns = 80, phase = 0, color = false) {
33
34
  function sisuWordmark() {
34
35
  return ['思溯', 'SISU'].join('\n');
35
36
  }
36
- /** Terminal cells, not JS string length — CJK and ∞ must sit on the grid. */
37
37
  function displayWidth(text) {
38
- let width = 0;
39
- for (const ch of stripAnsi(text)) {
40
- const code = ch.codePointAt(0) ?? 0;
41
- width += isWide(code) ? 2 : 1;
42
- }
43
- return width;
44
- }
45
- function isWide(code) {
46
- return ((code >= 0x1100 && code <= 0x115f) ||
47
- (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
48
- (code >= 0xac00 && code <= 0xd7a3) ||
49
- (code >= 0xf900 && code <= 0xfaff) ||
50
- (code >= 0xfe10 && code <= 0xfe19) ||
51
- (code >= 0xfe30 && code <= 0xfe6f) ||
52
- (code >= 0xff00 && code <= 0xff60) ||
53
- (code >= 0xffe0 && code <= 0xffe6));
38
+ return (0, theme_1.visibleWidth)(text);
54
39
  }
55
40
  function paint(text, prefix, color) {
56
41
  if (!color || !text)
@@ -67,7 +52,7 @@ function lockup(color) {
67
52
  const inf = infinityMark(color);
68
53
  const name = paint('思溯', INK, color);
69
54
  const latin = paint('SiSu', INK_DIM, color);
70
- return [`${inf} ${name}`, ` ${latin}`];
55
+ return [`${inf} ${name}`, latin];
71
56
  }
72
57
  function padLeft(text, n) {
73
58
  return `${' '.repeat(Math.max(0, n))}${text}`;
package/dist/pager/app.js CHANGED
@@ -11,18 +11,23 @@ const logo_1 = require("../logo");
11
11
  const render_1 = require("./render");
12
12
  const ALT_ENTER = '\x1b[?1049h\x1b[?25l';
13
13
  const ALT_LEAVE = '\x1b[?1049l\x1b[?25h';
14
- function formatChromeStatus(email, quota, conversationId) {
14
+ function shortConversationId(id) {
15
+ const conv = (id || '').trim();
16
+ if (!conv || conv === 'new')
17
+ return '';
18
+ return conv.length > 12 ? conv.slice(0, 8) : conv;
19
+ }
20
+ function formatChromeStatus(email, quota, conversationId = '') {
15
21
  const who = (email || '').trim();
22
+ const conv = shortConversationId(conversationId);
16
23
  if (!who) {
17
- const conv = (conversationId || '').trim();
18
- return conv && conv !== 'new' ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
24
+ return conv ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
19
25
  }
20
26
  const parts = [who];
21
27
  const quotaText = (quota || '').trim();
22
28
  if (quotaText && quotaText !== 'quota unavailable')
23
29
  parts.push(quotaText);
24
- const conv = (conversationId || '').trim();
25
- if (conv && conv !== 'new')
30
+ if (conv)
26
31
  parts.push(conv);
27
32
  return parts.join(' · ');
28
33
  }
@@ -106,7 +111,7 @@ async function runPager(io, transport, options = {}) {
106
111
  const paint = (phase = logo_1.SISU_STILL_PHASE) => {
107
112
  if (!running)
108
113
  return;
109
- io.write(`\x1b[H${(0, render_1.renderPager)(state, cols, rows, theme, { phase })}`);
114
+ io.write(`\x1b[H${(0, render_1.renderPager)(state, cols, rows, theme, { phase })}\x1b[J`);
110
115
  };
111
116
  const playIntro = async () => {
112
117
  const frames = Math.max(2, options.introFrames ?? 32);
@@ -211,24 +216,24 @@ async function runPager(io, transport, options = {}) {
211
216
  paint();
212
217
  return;
213
218
  }
214
- state = pushEntry(state, 'status', 'Opening browser to sign in…');
219
+ state = { ...state, statusLine: 'Opening browser to sign in…' };
215
220
  paint();
216
221
  try {
217
222
  const email = await options.login((line) => {
218
223
  if (!running)
219
224
  return;
220
- state = pushEntry(state, 'status', line);
225
+ state = { ...state, statusLine: line };
221
226
  paint();
222
227
  });
223
228
  if (!running)
224
229
  return;
225
230
  chromeEmail = email;
226
231
  state = withChrome(state);
227
- state = pushEntry(state, 'status', `logged in as ${email}`);
228
232
  }
229
233
  catch (err) {
230
234
  if (!running)
231
235
  return;
236
+ state = withChrome(state);
232
237
  state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
233
238
  }
234
239
  paint();
@@ -7,25 +7,13 @@ const mobius_1 = require("../mobius");
7
7
  const model_1 = require("./model");
8
8
  const theme_1 = require("./theme");
9
9
  Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
10
+ const text_1 = require("./text");
10
11
  const PROMPT_PREFIX = '› ';
11
12
  const PROMPT_BOX_ROWS = 2;
12
13
  const STATUS_ROWS = 1;
13
14
  const MARK_WIDTH = 2;
14
15
  function wrapPlain(text, width) {
15
- const max = Math.max(1, width);
16
- if (!text)
17
- return [''];
18
- const out = [];
19
- for (const paragraph of text.split('\n')) {
20
- if (!paragraph) {
21
- out.push('');
22
- continue;
23
- }
24
- for (let i = 0; i < paragraph.length; i += max) {
25
- out.push(paragraph.slice(i, i + max));
26
- }
27
- }
28
- return out;
16
+ return (0, text_1.wrapCells)(text, width);
29
17
  }
30
18
  function lineCount(text) {
31
19
  if (!text)
@@ -43,22 +31,25 @@ function paintKind(kind, body, theme) {
43
31
  return theme.dim(body);
44
32
  return theme.text(body);
45
33
  }
46
- function entryPrefix(kind) {
47
- if (kind === 'user')
48
- return 'you ';
49
- if (kind === 'tool')
50
- return 'tool ';
51
- return '';
34
+ function shownText(entry) {
35
+ if (entry.kind === 'assistant')
36
+ return (0, text_1.visibleAssistantText)(entry.text);
37
+ return entry.text;
52
38
  }
53
39
  function entryBodyLines(entry, wrapWidth) {
40
+ const text = shownText(entry);
54
41
  if (entry.folded) {
55
- const n = lineCount(entry.text);
42
+ const n = lineCount(text);
56
43
  const unit = n === 1 ? 'line' : 'lines';
57
44
  return wrapPlain(`${entry.kind} · ${n} ${unit}`, wrapWidth);
58
45
  }
59
- if (!entry.text)
46
+ if (entry.kind === 'user')
47
+ return ['You', ...wrapPlain(text || ' ', wrapWidth)];
48
+ if (entry.kind === 'tool')
49
+ return wrapPlain(text ? `tool ${text}` : 'tool', wrapWidth);
50
+ if (!text)
60
51
  return [''];
61
- return wrapPlain(`${entryPrefix(entry.kind)}${entry.text}`, wrapWidth);
52
+ return wrapPlain(text, wrapWidth);
62
53
  }
63
54
  function layoutScrollback(state, cols, theme) {
64
55
  const wrapWidth = Math.max(1, cols - MARK_WIDTH);
@@ -68,6 +59,10 @@ function layoutScrollback(state, cols, theme) {
68
59
  const entry = state.entries[i];
69
60
  const body = entryBodyLines(entry, wrapWidth);
70
61
  const selected = i === state.selected && state.entries.length > 0;
62
+ if (i > 0) {
63
+ lines.push('');
64
+ entryOf.push(i);
65
+ }
71
66
  for (let j = 0; j < body.length; j += 1) {
72
67
  const mark = selected && j === 0 ? theme.accent('▸ ') : ' ';
73
68
  lines.push(`${mark}${paintKind(entry.kind, body[j], theme)}`);
@@ -156,13 +151,17 @@ function renderPager(state, cols, rows, themeName = 'dark', view = {}) {
156
151
  const laid = layoutScrollback(state, width, theme);
157
152
  const lastEntry = Math.max(0, state.entries.length - 1);
158
153
  const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
159
- const body = [];
160
- body.push(...welcome.slice(0, welcomeTake));
161
- while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
162
- body.push('');
163
- }
164
- body.push(...visibleScroll);
165
- body.push(...slash.slice(0, slashTake));
154
+ const shownWelcome = welcome.slice(0, welcomeTake);
155
+ const extra = Math.max(0, welcomeTake + scrollBudget - shownWelcome.length - visibleScroll.length);
156
+ const padTop = isIdleWelcome(state) && visibleScroll.length === 0 ? Math.floor(extra / 2) : 0;
157
+ const padBot = extra - padTop;
158
+ const body = [
159
+ ...Array.from({ length: padTop }, () => ''),
160
+ ...shownWelcome,
161
+ ...Array.from({ length: padBot }, () => ''),
162
+ ...visibleScroll,
163
+ ...slash.slice(0, slashTake),
164
+ ];
166
165
  const lines = body.map((line) => (0, theme_1.padVisible)(line, width));
167
166
  if (statusRows > 0) {
168
167
  lines.push((0, theme_1.padVisible)(theme.dim(state.statusLine ?? ''), width));
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /** Display-side text: hide model scratch and wrap by terminal cells. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.visibleAssistantText = visibleAssistantText;
5
+ exports.wrapCells = wrapCells;
6
+ const theme_1 = require("./theme");
7
+ const THINK_BLOCK = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
8
+ const THINK_OPEN = /<think\b[^>]*>[\s\S]*$/i;
9
+ function visibleAssistantText(raw) {
10
+ let text = String(raw || '');
11
+ text = text.replace(THINK_BLOCK, '');
12
+ text = text.replace(THINK_OPEN, '');
13
+ return text.replace(/^\n+/, '').replace(/\n+$/, '');
14
+ }
15
+ function wrapCells(text, width) {
16
+ const max = Math.max(1, width);
17
+ if (!text)
18
+ return [''];
19
+ const out = [];
20
+ for (const paragraph of text.split('\n')) {
21
+ if (!paragraph) {
22
+ out.push('');
23
+ continue;
24
+ }
25
+ let line = '';
26
+ for (const ch of paragraph) {
27
+ if ((0, theme_1.visibleWidth)(line + ch) > max) {
28
+ if (line)
29
+ out.push(line);
30
+ line = ch;
31
+ }
32
+ else {
33
+ line += ch;
34
+ }
35
+ }
36
+ if (line)
37
+ out.push(line);
38
+ }
39
+ return out.length ? out : [''];
40
+ }
@@ -42,8 +42,26 @@ function getTheme(name = 'dark') {
42
42
  function stripAnsi(s) {
43
43
  return s.replace(/\x1b\[[0-9;]*m/g, '');
44
44
  }
45
+ function isWide(code) {
46
+ return ((code >= 0x1100 && code <= 0x115f) ||
47
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
48
+ (code >= 0xac00 && code <= 0xd7a3) ||
49
+ (code >= 0xf900 && code <= 0xfaff) ||
50
+ (code >= 0xfe10 && code <= 0xfe19) ||
51
+ (code >= 0xfe30 && code <= 0xfe6f) ||
52
+ (code >= 0xff00 && code <= 0xff60) ||
53
+ (code >= 0xffe0 && code <= 0xffe6));
54
+ }
55
+ function charWidth(ch) {
56
+ const code = ch.codePointAt(0) ?? 0;
57
+ return isWide(code) ? 2 : 1;
58
+ }
59
+ /** Terminal cells, not JS string length — CJK is two cells. */
45
60
  function visibleWidth(line) {
46
- return stripAnsi(line).length;
61
+ let width = 0;
62
+ for (const ch of stripAnsi(line))
63
+ width += charWidth(ch);
64
+ return width;
47
65
  }
48
66
  function clipVisible(line, width) {
49
67
  if (width <= 0)
@@ -55,23 +73,29 @@ function clipVisible(line, width) {
55
73
  const re = /\x1b\[[0-9;]*m/g;
56
74
  let last = 0;
57
75
  let match = re.exec(line);
58
- while (match) {
59
- for (let i = last; i < match.index && seen < width; i += 1) {
60
- out += line[i];
61
- seen += 1;
76
+ const take = (from, to) => {
77
+ let i = from;
78
+ while (i < to && seen < width) {
79
+ const code = line.charCodeAt(i);
80
+ const wide = code >= 0xd800 && code <= 0xdbff;
81
+ const ch = wide ? line.slice(i, i + 2) : line[i];
82
+ const step = charWidth(ch);
83
+ if (seen + step > width)
84
+ break;
85
+ out += ch;
86
+ seen += step;
87
+ i += ch.length;
62
88
  }
89
+ };
90
+ while (match) {
91
+ take(last, match.index);
63
92
  if (seen >= width)
64
93
  break;
65
94
  out += match[0];
66
95
  last = match.index + match[0].length;
67
96
  match = re.exec(line);
68
97
  }
69
- for (let i = last; i < line.length && seen < width; i += 1) {
70
- if (line[i] === '\x1b')
71
- break;
72
- out += line[i];
73
- seen += 1;
74
- }
98
+ take(last, line.length);
75
99
  return out + RESET;
76
100
  }
77
101
  function padVisible(line, cols) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",