@stevezhou/sisu 0.1.2 → 0.1.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.
package/dist/logo.js CHANGED
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sisuMarkLines = exports.sisuMarkArt = void 0;
3
4
  exports.sisuMobiusArt = sisuMobiusArt;
4
5
  exports.sisuWordmark = sisuWordmark;
6
+ exports.sisuSplash = sisuSplash;
5
7
  exports.sisuBanner = sisuBanner;
6
8
  exports.stripAnsi = stripAnsi;
9
+ var mark_1 = require("./mark");
10
+ Object.defineProperty(exports, "sisuMarkArt", { enumerable: true, get: function () { return mark_1.sisuMarkArt; } });
11
+ Object.defineProperty(exports, "sisuMarkLines", { enumerable: true, get: function () { return mark_1.sisuMarkLines; } });
12
+ const mark_2 = require("./mark");
7
13
  const mobius_1 = require("./mobius");
8
14
  function sisuMobiusArt(columns = 80, phase = 0, color = false) {
9
15
  return (0, mobius_1.renderMobiusFrame)({
@@ -16,14 +22,16 @@ function sisuMobiusArt(columns = 80, phase = 0, color = false) {
16
22
  function sisuWordmark() {
17
23
  return ['思溯', 'SISU'].join('\n');
18
24
  }
19
- function sisuBanner(columns = 80, phase = 0, color = false) {
20
- return [
21
- '',
22
- sisuMobiusArt(columns, phase, color),
23
- '',
24
- sisuWordmark(),
25
- '',
26
- ].join('\n');
25
+ /** Splash used on TUI enter: the web Möbius mark plus 思溯. */
26
+ function sisuSplash(columns = 80, color = true) {
27
+ const mark = (0, mark_2.sisuMarkArt)(columns, color);
28
+ const markWidth = (0, mark_2.sisuMarkLines)(columns)[0]?.length ?? 0;
29
+ const caption = '思溯 SISU';
30
+ const pad = Math.max(0, Math.floor((markWidth - caption.length) / 2));
31
+ return ['', mark, '', `${' '.repeat(pad)}${caption}`, ''].join('\n');
32
+ }
33
+ function sisuBanner(columns = 80, _phase = 0, color = false) {
34
+ return sisuSplash(columns, color);
27
35
  }
28
36
  function stripAnsi(text) {
29
37
  return text.replace(/\x1b\[[0-9;]*m/g, '');
package/dist/mark.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /** Hand-drawn SiSu Möbius mark — the web ∞ ribbon, not a 3D rasterizer. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.markRgb = markRgb;
5
+ exports.sisuMarkLines = sisuMarkLines;
6
+ exports.sisuMarkArt = sisuMarkArt;
7
+ exports.sisuMarkWidth = sisuMarkWidth;
8
+ exports.sisuMarkHeight = sisuMarkHeight;
9
+ const RESET = '\x1b[0m';
10
+ const MARK_WIDE = [
11
+ ' ▄▄████▄▄ ▄▄████▄▄ ',
12
+ ' ▄██▀ ▀██▄ ▄██▀ ▀██▄ ',
13
+ ' ██ ▀█▄ ▄█▀ ██ ',
14
+ ' ██ ▀████▀ ██ ',
15
+ ' ██ ▄████▄ ██ ',
16
+ ' ██ ▄█▀ ▀█▄ ██ ',
17
+ ' ▀██▄ ▄██▀ ▀██▄ ▄██▀ ',
18
+ ' ▀▀████▀▀ ▀▀████▀▀ ',
19
+ ];
20
+ const MARK_NARROW = [
21
+ ' ▄██▄ ▄██▄ ',
22
+ ' █▀ ▀█▄ ▄█▀ ▀█ ',
23
+ ' █ ▀██▀ █ ',
24
+ ' █ ▄██▄ █ ',
25
+ ' █▄ ▄█▀ ▀█▄ ▄█ ',
26
+ ' ▀██▀ ▀██▀ ',
27
+ ];
28
+ function lerp(a, b, t) {
29
+ return a + (b - a) * t;
30
+ }
31
+ /** Brand gradient along the ribbon: blue → purple → gold. */
32
+ function markRgb(x, width) {
33
+ const t = width <= 1 ? 0 : Math.max(0, Math.min(1, x / (width - 1)));
34
+ if (t < 0.5) {
35
+ const k = t / 0.5;
36
+ return [
37
+ Math.round(lerp(37, 124, k)),
38
+ Math.round(lerp(99, 58, k)),
39
+ Math.round(lerp(235, 237, k)),
40
+ ];
41
+ }
42
+ const k = (t - 0.5) / 0.5;
43
+ return [
44
+ Math.round(lerp(124, 217, k)),
45
+ Math.round(lerp(58, 119, k)),
46
+ Math.round(lerp(237, 6, k)),
47
+ ];
48
+ }
49
+ function paintLine(line) {
50
+ const width = line.length;
51
+ let out = '';
52
+ for (let i = 0; i < line.length; i += 1) {
53
+ const ch = line[i];
54
+ if (ch === ' ') {
55
+ out += ' ';
56
+ continue;
57
+ }
58
+ const [r, g, b] = markRgb(i, width);
59
+ out += `\x1b[38;2;${r};${g};${b}m${ch}`;
60
+ }
61
+ return `${out}${RESET}`;
62
+ }
63
+ function sisuMarkLines(columns = 80) {
64
+ return columns >= 48 ? MARK_WIDE : MARK_NARROW;
65
+ }
66
+ function sisuMarkArt(columns = 80, color = true) {
67
+ const lines = sisuMarkLines(columns);
68
+ const painted = color ? lines.map(paintLine) : lines;
69
+ return painted.join('\n');
70
+ }
71
+ function sisuMarkWidth(columns = 80) {
72
+ return sisuMarkLines(columns).reduce((max, line) => Math.max(max, line.length), 0);
73
+ }
74
+ function sisuMarkHeight(columns = 80) {
75
+ return sisuMarkLines(columns).length;
76
+ }
@@ -1,24 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripAnsi = void 0;
3
4
  exports.renderPager = renderPager;
5
+ const mark_1 = require("../mark");
4
6
  const model_1 = require("./model");
5
7
  const theme_1 = require("./theme");
8
+ Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
6
9
  const PROMPT_PREFIX = '› ';
7
10
  const PROMPT_BOX_ROWS = 2;
8
11
  const STATUS_ROWS = 1;
9
12
  const MARK_WIDTH = 2;
10
- /** Pad (and clip reserved chrome) to exactly `cols` cells. */
11
- function pad(line, cols) {
12
- const width = Math.max(0, cols);
13
- if (width === 0)
14
- return '';
15
- const plain = (0, theme_1.stripAnsi)(line);
16
- if (plain.length === width)
17
- return plain;
18
- if (plain.length > width)
19
- return plain.slice(0, width);
20
- return plain.padEnd(width, ' ');
21
- }
22
13
  function wrapPlain(text, width) {
23
14
  const max = Math.max(1, width);
24
15
  if (!text)
@@ -40,6 +31,24 @@ function lineCount(text) {
40
31
  return 0;
41
32
  return text.split('\n').length;
42
33
  }
34
+ function paintKind(kind, body, theme) {
35
+ if (!body)
36
+ return body;
37
+ if (kind === 'user')
38
+ return theme.user(body);
39
+ if (kind === 'tool')
40
+ return theme.tool(body);
41
+ if (kind === 'status')
42
+ return theme.dim(body);
43
+ return theme.text(body);
44
+ }
45
+ function entryPrefix(kind) {
46
+ if (kind === 'user')
47
+ return 'you ';
48
+ if (kind === 'tool')
49
+ return 'tool ';
50
+ return '';
51
+ }
43
52
  function entryBodyLines(entry, wrapWidth) {
44
53
  if (entry.folded) {
45
54
  const n = lineCount(entry.text);
@@ -48,18 +57,19 @@ function entryBodyLines(entry, wrapWidth) {
48
57
  }
49
58
  if (!entry.text)
50
59
  return [''];
51
- return wrapPlain(entry.text, wrapWidth);
60
+ return wrapPlain(`${entryPrefix(entry.kind)}${entry.text}`, wrapWidth);
52
61
  }
53
- function layoutScrollback(state, cols) {
62
+ function layoutScrollback(state, cols, theme) {
54
63
  const wrapWidth = Math.max(1, cols - MARK_WIDTH);
55
64
  const lines = [];
56
65
  const entryOf = [];
57
66
  for (let i = 0; i < state.entries.length; i += 1) {
58
67
  const entry = state.entries[i];
59
68
  const body = entryBodyLines(entry, wrapWidth);
60
- const mark = i === state.selected && state.entries.length > 0 ? '▸ ' : ' ';
69
+ const selected = i === state.selected && state.entries.length > 0;
61
70
  for (let j = 0; j < body.length; j += 1) {
62
- lines.push(j === 0 ? `${mark}${body[j]}` : ` ${body[j]}`);
71
+ const mark = selected && j === 0 ? theme.accent('▸ ') : ' ';
72
+ lines.push(`${mark}${paintKind(entry.kind, body[j], theme)}`);
63
73
  entryOf.push(i);
64
74
  }
65
75
  }
@@ -91,40 +101,46 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
91
101
  function isIdleWelcome(state) {
92
102
  return !state.conversationId && state.entries.length === 0;
93
103
  }
94
- function welcomeLines(guest) {
104
+ function center(text, width, vis = (0, theme_1.stripAnsi)(text).length) {
105
+ const padLeft = Math.max(0, Math.floor((width - vis) / 2));
106
+ return `${' '.repeat(padLeft)}${text}`;
107
+ }
108
+ function welcomeLines(guest, width, height, theme) {
109
+ const markCols = height >= 20 && width >= 48 ? width : 40;
110
+ const raw = (0, mark_1.sisuMarkLines)(markCols);
111
+ const painted = (0, mark_1.sisuMarkArt)(markCols, true).split('\n');
112
+ const mark = painted.map((line, i) => center(line, width, raw[i]?.length ?? (0, theme_1.stripAnsi)(line).length));
95
113
  if (guest) {
96
114
  return [
97
- 'SISU',
115
+ ...mark,
98
116
  '',
99
- 'Sign in to start a conversation.',
100
- '/login open the browser',
101
- '/help commands',
117
+ theme.text(center('Sign in to start a conversation.', width)),
118
+ theme.dim(center('/login browser /help commands', width)),
102
119
  ];
103
120
  }
104
121
  return [
105
- 'SISU',
122
+ ...mark,
106
123
  '',
107
- 'Ask anything, or type /help.',
124
+ theme.dim(center('Ask anything, or type /help.', width)),
108
125
  ];
109
126
  }
110
- function slashMenuLines(state) {
127
+ function slashMenuLines(state, theme) {
111
128
  if (!state.slashOpen)
112
129
  return [];
113
130
  const items = (0, model_1.filterSlash)(state.draft);
114
131
  return items.map((item, index) => {
115
- const mark = index === state.slashIndex ? '› ' : ' ';
116
- return `${mark}${item.name} ${item.hint}`;
132
+ const active = index === state.slashIndex;
133
+ const mark = active ? theme.accent('› ') : ' ';
134
+ const name = active ? theme.accent(item.name) : theme.text(item.name);
135
+ return `${mark}${name} ${theme.dim(item.hint)}`;
117
136
  });
118
137
  }
119
138
  /**
120
- * Pure fixed-grid frame: always `rows` lines, each exactly `cols` characters.
121
- * `theme` selects the SiSu palette via `getTheme` for future colored paint; the
122
- * frame itself is plain so `line.length === cols` for the tty writer.
123
- * Deterministic: no clock, no I/O.
139
+ * Fixed-grid frame: always `rows` lines, each `cols` visible cells.
140
+ * Colors are 24-bit SGR; measure width with stripAnsi / visibleWidth.
124
141
  */
125
- function renderPager(state, cols, rows, theme = 'dark') {
126
- // Bind palette so dark/light stays on the public pure path (app may recolor).
127
- (0, theme_1.getTheme)(theme);
142
+ function renderPager(state, cols, rows, themeName = 'dark') {
143
+ const theme = (0, theme_1.getTheme)(themeName);
128
144
  const height = Math.max(0, rows);
129
145
  const width = Math.max(0, cols);
130
146
  if (height === 0)
@@ -133,38 +149,36 @@ function renderPager(state, cols, rows, theme = 'dark') {
133
149
  const statusRows = height > promptRows ? Math.min(STATUS_ROWS, height - promptRows) : 0;
134
150
  const chrome = promptRows + statusRows;
135
151
  const bodyBudget = Math.max(0, height - chrome);
136
- const slash = slashMenuLines(state);
152
+ const slash = slashMenuLines(state, theme);
137
153
  const slashTake = Math.min(slash.length, bodyBudget);
138
154
  const guest = !(state.statusLine || '').trim() || (state.statusLine || '').includes('not signed in');
139
- const welcome = isIdleWelcome(state) ? welcomeLines(guest).map((line) => (line ? ` ${line}` : '')) : [];
155
+ const welcome = isIdleWelcome(state) ? welcomeLines(guest, width, height, theme) : [];
140
156
  const welcomeTake = Math.min(welcome.length, Math.max(0, bodyBudget - slashTake));
141
157
  const scrollBudget = bodyBudget - slashTake - welcomeTake;
142
- const laid = layoutScrollback(state, width);
158
+ const laid = layoutScrollback(state, width, theme);
143
159
  const lastEntry = Math.max(0, state.entries.length - 1);
144
160
  const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
145
161
  const body = [];
146
162
  body.push(...welcome.slice(0, welcomeTake));
147
- // Top-pad remaining scrollback so live status sits just above slash/prompt.
148
163
  while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
149
164
  body.push('');
150
165
  }
151
166
  body.push(...visibleScroll);
152
167
  body.push(...slash.slice(0, slashTake));
153
- const lines = body.map((line) => pad(line, width));
168
+ const lines = body.map((line) => (0, theme_1.padVisible)(line, width));
154
169
  if (statusRows > 0) {
155
- lines.push(pad(state.statusLine ?? '', width));
170
+ lines.push((0, theme_1.padVisible)(theme.dim(state.statusLine ?? ''), width));
156
171
  }
157
172
  if (promptRows >= 2) {
158
- // Prompt box: border row + draft row (`› {draft}`).
159
- lines.push(pad('─'.repeat(width), width));
160
- lines.push(pad(`${PROMPT_PREFIX}${state.draft}`, width));
173
+ lines.push((0, theme_1.padVisible)(theme.border('─'.repeat(width)), width));
174
+ lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
161
175
  }
162
176
  else if (promptRows === 1) {
163
- lines.push(pad(`${PROMPT_PREFIX}${state.draft}`, width));
177
+ lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
164
178
  }
165
179
  while (lines.length < height)
166
- lines.push(pad('', width));
180
+ lines.push((0, theme_1.padVisible)('', width));
167
181
  if (lines.length > height)
168
182
  lines.length = height;
169
- return lines.map((line) => pad(line, width)).join('\n');
183
+ return lines.map((line) => (0, theme_1.padVisible)(line, width)).join('\n');
170
184
  }
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
- /** SiSu pager palette: blue → purple → gold (same anchors as mobiusRgb). */
2
+ /** SiSu pager palette: blue → purple → gold. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.getTheme = getTheme;
5
5
  exports.stripAnsi = stripAnsi;
6
+ exports.visibleWidth = visibleWidth;
7
+ exports.clipVisible = clipVisible;
8
+ exports.padVisible = padVisible;
6
9
  const RESET = '\x1b[0m';
7
10
  function ansiRgb(r, g, b) {
8
11
  return `\x1b[38;2;${r};${g};${b}m`;
@@ -10,16 +13,17 @@ function ansiRgb(r, g, b) {
10
13
  function paint(prefix) {
11
14
  return (s) => (s ? `${prefix}${s}${RESET}` : s);
12
15
  }
13
- /** Brand anchors from mobiusRgb (blue, purple, gold). */
14
16
  const BLUE = ansiRgb(37, 99, 235);
15
17
  const PURPLE = ansiRgb(124, 58, 237);
16
18
  const GOLD = ansiRgb(217, 119, 6);
17
19
  const DARK = {
18
20
  text: paint(ansiRgb(230, 232, 240)),
19
- dim: paint(ansiRgb(120, 126, 150)),
21
+ dim: paint(ansiRgb(118, 124, 148)),
20
22
  accent: paint(GOLD),
21
23
  error: paint(ansiRgb(239, 68, 68)),
22
- border: paint(PURPLE),
24
+ border: paint(ansiRgb(72, 64, 96)),
25
+ user: paint(ansiRgb(156, 174, 214)),
26
+ tool: paint(PURPLE),
23
27
  reset: RESET,
24
28
  };
25
29
  const LIGHT = {
@@ -28,12 +32,56 @@ const LIGHT = {
28
32
  accent: paint(GOLD),
29
33
  error: paint(ansiRgb(185, 28, 28)),
30
34
  border: paint(BLUE),
35
+ user: paint(ansiRgb(37, 99, 180)),
36
+ tool: paint(PURPLE),
31
37
  reset: RESET,
32
38
  };
33
39
  function getTheme(name = 'dark') {
34
40
  return name === 'light' ? LIGHT : DARK;
35
41
  }
36
- /** Strip 24-bit / SGR sequences for visible-width measurement. */
37
42
  function stripAnsi(s) {
38
43
  return s.replace(/\x1b\[[0-9;]*m/g, '');
39
44
  }
45
+ function visibleWidth(line) {
46
+ return stripAnsi(line).length;
47
+ }
48
+ function clipVisible(line, width) {
49
+ if (width <= 0)
50
+ return '';
51
+ if (visibleWidth(line) <= width)
52
+ return line;
53
+ let seen = 0;
54
+ let out = '';
55
+ const re = /\x1b\[[0-9;]*m/g;
56
+ let last = 0;
57
+ 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;
62
+ }
63
+ if (seen >= width)
64
+ break;
65
+ out += match[0];
66
+ last = match.index + match[0].length;
67
+ match = re.exec(line);
68
+ }
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
+ }
75
+ return out + RESET;
76
+ }
77
+ function padVisible(line, cols) {
78
+ const width = Math.max(0, cols);
79
+ if (width === 0)
80
+ return '';
81
+ const vis = visibleWidth(line);
82
+ if (vis === width)
83
+ return line;
84
+ if (vis > width)
85
+ return clipVisible(line, width);
86
+ return `${line}${' '.repeat(width - vis)}`;
87
+ }
package/dist/tui.js CHANGED
@@ -174,7 +174,7 @@ async function runTui(io, deps = {}) {
174
174
  const columns = deps.columns ?? process.stdout.columns ?? 80;
175
175
  const animate = deps.animate ?? shouldAnimateSplash();
176
176
  try {
177
- io.write(`\n${(0, logo_1.sisuWordmark)()}\n\n`);
177
+ io.write(`${(0, logo_1.sisuSplash)(columns, true)}\n`);
178
178
  if (animate) {
179
179
  await (deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(80);
180
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",