@stevezhou/sisu 0.1.2 → 0.1.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.
@@ -1,24 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripAnsi = void 0;
3
4
  exports.renderPager = renderPager;
4
5
  const model_1 = require("./model");
5
6
  const theme_1 = require("./theme");
7
+ Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
6
8
  const PROMPT_PREFIX = '› ';
7
9
  const PROMPT_BOX_ROWS = 2;
8
10
  const STATUS_ROWS = 1;
9
11
  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
12
  function wrapPlain(text, width) {
23
13
  const max = Math.max(1, width);
24
14
  if (!text)
@@ -40,6 +30,24 @@ function lineCount(text) {
40
30
  return 0;
41
31
  return text.split('\n').length;
42
32
  }
33
+ function paintKind(kind, body, theme) {
34
+ if (!body)
35
+ return body;
36
+ if (kind === 'user')
37
+ return theme.user(body);
38
+ if (kind === 'tool')
39
+ return theme.tool(body);
40
+ if (kind === 'status')
41
+ return theme.dim(body);
42
+ return theme.text(body);
43
+ }
44
+ function entryPrefix(kind) {
45
+ if (kind === 'user')
46
+ return 'you ';
47
+ if (kind === 'tool')
48
+ return 'tool ';
49
+ return '';
50
+ }
43
51
  function entryBodyLines(entry, wrapWidth) {
44
52
  if (entry.folded) {
45
53
  const n = lineCount(entry.text);
@@ -48,18 +56,19 @@ function entryBodyLines(entry, wrapWidth) {
48
56
  }
49
57
  if (!entry.text)
50
58
  return [''];
51
- return wrapPlain(entry.text, wrapWidth);
59
+ return wrapPlain(`${entryPrefix(entry.kind)}${entry.text}`, wrapWidth);
52
60
  }
53
- function layoutScrollback(state, cols) {
61
+ function layoutScrollback(state, cols, theme) {
54
62
  const wrapWidth = Math.max(1, cols - MARK_WIDTH);
55
63
  const lines = [];
56
64
  const entryOf = [];
57
65
  for (let i = 0; i < state.entries.length; i += 1) {
58
66
  const entry = state.entries[i];
59
67
  const body = entryBodyLines(entry, wrapWidth);
60
- const mark = i === state.selected && state.entries.length > 0 ? '▸ ' : ' ';
68
+ const selected = i === state.selected && state.entries.length > 0;
61
69
  for (let j = 0; j < body.length; j += 1) {
62
- lines.push(j === 0 ? `${mark}${body[j]}` : ` ${body[j]}`);
70
+ const mark = selected && j === 0 ? theme.accent('▸ ') : ' ';
71
+ lines.push(`${mark}${paintKind(entry.kind, body[j], theme)}`);
63
72
  entryOf.push(i);
64
73
  }
65
74
  }
@@ -91,40 +100,45 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
91
100
  function isIdleWelcome(state) {
92
101
  return !state.conversationId && state.entries.length === 0;
93
102
  }
94
- function welcomeLines(guest) {
103
+ function center(text, width) {
104
+ const padLeft = Math.max(0, Math.floor((width - text.length) / 2));
105
+ return `${' '.repeat(padLeft)}${text}`;
106
+ }
107
+ function welcomeLines(guest, width, theme) {
108
+ const title = theme.accent(center('SISU', width));
95
109
  if (guest) {
96
110
  return [
97
- 'SISU',
111
+ title,
112
+ '',
113
+ theme.text(center('Sign in to start a conversation.', width)),
98
114
  '',
99
- 'Sign in to start a conversation.',
100
- '/login open the browser',
101
- '/help commands',
115
+ theme.dim(center('/login browser', width)),
116
+ theme.dim(center('/help commands', width)),
102
117
  ];
103
118
  }
104
119
  return [
105
- 'SISU',
120
+ title,
106
121
  '',
107
- 'Ask anything, or type /help.',
122
+ theme.dim(center('Ask anything, or type /help.', width)),
108
123
  ];
109
124
  }
110
- function slashMenuLines(state) {
125
+ function slashMenuLines(state, theme) {
111
126
  if (!state.slashOpen)
112
127
  return [];
113
128
  const items = (0, model_1.filterSlash)(state.draft);
114
129
  return items.map((item, index) => {
115
- const mark = index === state.slashIndex ? '› ' : ' ';
116
- return `${mark}${item.name} ${item.hint}`;
130
+ const active = index === state.slashIndex;
131
+ const mark = active ? theme.accent('› ') : ' ';
132
+ const name = active ? theme.accent(item.name) : theme.text(item.name);
133
+ return `${mark}${name} ${theme.dim(item.hint)}`;
117
134
  });
118
135
  }
119
136
  /**
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.
137
+ * Fixed-grid frame: always `rows` lines, each `cols` visible cells.
138
+ * Colors are 24-bit SGR; measure width with stripAnsi / visibleWidth.
124
139
  */
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);
140
+ function renderPager(state, cols, rows, themeName = 'dark') {
141
+ const theme = (0, theme_1.getTheme)(themeName);
128
142
  const height = Math.max(0, rows);
129
143
  const width = Math.max(0, cols);
130
144
  if (height === 0)
@@ -133,38 +147,36 @@ function renderPager(state, cols, rows, theme = 'dark') {
133
147
  const statusRows = height > promptRows ? Math.min(STATUS_ROWS, height - promptRows) : 0;
134
148
  const chrome = promptRows + statusRows;
135
149
  const bodyBudget = Math.max(0, height - chrome);
136
- const slash = slashMenuLines(state);
150
+ const slash = slashMenuLines(state, theme);
137
151
  const slashTake = Math.min(slash.length, bodyBudget);
138
152
  const guest = !(state.statusLine || '').trim() || (state.statusLine || '').includes('not signed in');
139
- const welcome = isIdleWelcome(state) ? welcomeLines(guest).map((line) => (line ? ` ${line}` : '')) : [];
153
+ const welcome = isIdleWelcome(state) ? welcomeLines(guest, width, theme) : [];
140
154
  const welcomeTake = Math.min(welcome.length, Math.max(0, bodyBudget - slashTake));
141
155
  const scrollBudget = bodyBudget - slashTake - welcomeTake;
142
- const laid = layoutScrollback(state, width);
156
+ const laid = layoutScrollback(state, width, theme);
143
157
  const lastEntry = Math.max(0, state.entries.length - 1);
144
158
  const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
145
159
  const body = [];
146
160
  body.push(...welcome.slice(0, welcomeTake));
147
- // Top-pad remaining scrollback so live status sits just above slash/prompt.
148
161
  while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
149
162
  body.push('');
150
163
  }
151
164
  body.push(...visibleScroll);
152
165
  body.push(...slash.slice(0, slashTake));
153
- const lines = body.map((line) => pad(line, width));
166
+ const lines = body.map((line) => (0, theme_1.padVisible)(line, width));
154
167
  if (statusRows > 0) {
155
- lines.push(pad(state.statusLine ?? '', width));
168
+ lines.push((0, theme_1.padVisible)(theme.dim(state.statusLine ?? ''), width));
156
169
  }
157
170
  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));
171
+ lines.push((0, theme_1.padVisible)(theme.border('─'.repeat(width)), width));
172
+ lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
161
173
  }
162
174
  else if (promptRows === 1) {
163
- lines.push(pad(`${PROMPT_PREFIX}${state.draft}`, width));
175
+ lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
164
176
  }
165
177
  while (lines.length < height)
166
- lines.push(pad('', width));
178
+ lines.push((0, theme_1.padVisible)('', width));
167
179
  if (lines.length > height)
168
180
  lines.length = height;
169
- return lines.map((line) => pad(line, width)).join('\n');
181
+ return lines.map((line) => (0, theme_1.padVisible)(line, width)).join('\n');
170
182
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",