@stevezhou/sisu 0.1.1 → 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.
package/dist/commands.js CHANGED
@@ -126,8 +126,15 @@ async function webLoginCommand(input = {}, http = http_1.defaultHttp) {
126
126
  }
127
127
  const started = await http(`${apiBase}/api/auth/cli/device`, { method: 'POST' });
128
128
  const startBody = await started.json().catch(() => ({}));
129
- if (!started.ok)
129
+ if (!started.ok) {
130
+ if (started.status === 404 || started.status === 405) {
131
+ throw new Error('Browser login is not on this server yet. Use: sisu login --email <email> --password <password>');
132
+ }
133
+ if (started.status === 503) {
134
+ throw new Error('Sign-in is temporarily unavailable. Try again in a moment.');
135
+ }
130
136
  throw new Error((0, http_1.errorDetail)(startBody, `device start failed (${started.status})`));
137
+ }
131
138
  const deviceCode = String(startBody.device_code || '');
132
139
  const userCode = String(startBody.user_code || '');
133
140
  const rawVerify = String(startBody.verification_uri_complete || startBody.verification_uri || '');
package/dist/pager/app.js CHANGED
@@ -12,12 +12,18 @@ const ALT_ENTER = '\x1b[?1049h\x1b[?25l';
12
12
  const ALT_LEAVE = '\x1b[?1049l\x1b[?25h';
13
13
  const CLEAR_HOME = '\x1b[2J\x1b[H';
14
14
  function formatChromeStatus(email, quota, conversationId) {
15
- const parts = [
16
- (email || '').trim() || 'not logged in',
17
- (quota || '').trim() || 'quota unavailable',
18
- conversationId || 'new',
19
- 'client=tui',
20
- ].filter((part) => part.length > 0);
15
+ const who = (email || '').trim();
16
+ if (!who) {
17
+ const conv = (conversationId || '').trim();
18
+ return conv && conv !== 'new' ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
19
+ }
20
+ const parts = [who];
21
+ const quotaText = (quota || '').trim();
22
+ if (quotaText && quotaText !== 'quota unavailable')
23
+ parts.push(quotaText);
24
+ const conv = (conversationId || '').trim();
25
+ if (conv && conv !== 'new')
26
+ parts.push(conv);
21
27
  return parts.join(' · ');
22
28
  }
23
29
  /** Short chrome fragment: `quota unlimited`, first `quota N pts`, or `quota unavailable`. */
@@ -89,9 +95,6 @@ async function runPager(io, transport, options = {}) {
89
95
  }
90
96
  };
91
97
  let state = withChrome((0, model_1.createPagerState)());
92
- if (!chromeEmail && options.login) {
93
- state = pushEntry(state, 'status', 'Not logged in. Type /login to sign in with your browser.');
94
- }
95
98
  let rest = '';
96
99
  let newConversation = false;
97
100
  let pickMode = false;
@@ -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
  }
@@ -88,24 +97,48 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
88
97
  const windowStart = Math.max(0, Math.min(start, end + 1 - budget));
89
98
  return lines.slice(windowStart, windowStart + budget);
90
99
  }
91
- function slashMenuLines(state) {
100
+ function isIdleWelcome(state) {
101
+ return !state.conversationId && state.entries.length === 0;
102
+ }
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));
109
+ if (guest) {
110
+ return [
111
+ title,
112
+ '',
113
+ theme.text(center('Sign in to start a conversation.', width)),
114
+ '',
115
+ theme.dim(center('/login browser', width)),
116
+ theme.dim(center('/help commands', width)),
117
+ ];
118
+ }
119
+ return [
120
+ title,
121
+ '',
122
+ theme.dim(center('Ask anything, or type /help.', width)),
123
+ ];
124
+ }
125
+ function slashMenuLines(state, theme) {
92
126
  if (!state.slashOpen)
93
127
  return [];
94
128
  const items = (0, model_1.filterSlash)(state.draft);
95
129
  return items.map((item, index) => {
96
- const mark = index === state.slashIndex ? '› ' : ' ';
97
- 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)}`;
98
134
  });
99
135
  }
100
136
  /**
101
- * Pure fixed-grid frame: always `rows` lines, each exactly `cols` characters.
102
- * `theme` selects the SiSu palette via `getTheme` for future colored paint; the
103
- * frame itself is plain so `line.length === cols` for the tty writer.
104
- * 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.
105
139
  */
106
- function renderPager(state, cols, rows, theme = 'dark') {
107
- // Bind palette so dark/light stays on the public pure path (app may recolor).
108
- (0, theme_1.getTheme)(theme);
140
+ function renderPager(state, cols, rows, themeName = 'dark') {
141
+ const theme = (0, theme_1.getTheme)(themeName);
109
142
  const height = Math.max(0, rows);
110
143
  const width = Math.max(0, cols);
111
144
  if (height === 0)
@@ -114,34 +147,36 @@ function renderPager(state, cols, rows, theme = 'dark') {
114
147
  const statusRows = height > promptRows ? Math.min(STATUS_ROWS, height - promptRows) : 0;
115
148
  const chrome = promptRows + statusRows;
116
149
  const bodyBudget = Math.max(0, height - chrome);
117
- const slash = slashMenuLines(state);
150
+ const slash = slashMenuLines(state, theme);
118
151
  const slashTake = Math.min(slash.length, bodyBudget);
119
- const scrollBudget = bodyBudget - slashTake;
120
- const laid = layoutScrollback(state, width);
152
+ const guest = !(state.statusLine || '').trim() || (state.statusLine || '').includes('not signed in');
153
+ const welcome = isIdleWelcome(state) ? welcomeLines(guest, width, theme) : [];
154
+ const welcomeTake = Math.min(welcome.length, Math.max(0, bodyBudget - slashTake));
155
+ const scrollBudget = bodyBudget - slashTake - welcomeTake;
156
+ const laid = layoutScrollback(state, width, theme);
121
157
  const lastEntry = Math.max(0, state.entries.length - 1);
122
158
  const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
123
159
  const body = [];
124
- // Top-pad scrollback so newest content sits just above slash/status/prompt.
125
- while (body.length + visibleScroll.length < scrollBudget) {
160
+ body.push(...welcome.slice(0, welcomeTake));
161
+ while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
126
162
  body.push('');
127
163
  }
128
164
  body.push(...visibleScroll);
129
165
  body.push(...slash.slice(0, slashTake));
130
- const lines = body.map((line) => pad(line, width));
166
+ const lines = body.map((line) => (0, theme_1.padVisible)(line, width));
131
167
  if (statusRows > 0) {
132
- lines.push(pad(state.statusLine ?? '', width));
168
+ lines.push((0, theme_1.padVisible)(theme.dim(state.statusLine ?? ''), width));
133
169
  }
134
170
  if (promptRows >= 2) {
135
- // Prompt box: border row + draft row (`› {draft}`).
136
- lines.push(pad('─'.repeat(width), width));
137
- 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));
138
173
  }
139
174
  else if (promptRows === 1) {
140
- lines.push(pad(`${PROMPT_PREFIX}${state.draft}`, width));
175
+ lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
141
176
  }
142
177
  while (lines.length < height)
143
- lines.push(pad('', width));
178
+ lines.push((0, theme_1.padVisible)('', width));
144
179
  if (lines.length > height)
145
180
  lines.length = height;
146
- return lines.map((line) => pad(line, width)).join('\n');
181
+ return lines.map((line) => (0, theme_1.padVisible)(line, width)).join('\n');
147
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.1",
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",