@stevezhou/sisu 0.1.0

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/mobius.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /** Volumetric Möbius ring: half-twist ribbon with thickness, perspective, and lighting. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.mobiusRgb = mobiusRgb;
5
+ exports.mobiusPoint = mobiusPoint;
6
+ exports.renderMobiusFrame = renderMobiusFrame;
7
+ exports.mobiusFrameHeight = mobiusFrameHeight;
8
+ exports.mobiusFrameWidth = mobiusFrameWidth;
9
+ const RESET = '\x1b[0m';
10
+ const SHADE_RAMP = ' .:-=+*#%@';
11
+ const RADIUS = 1.88;
12
+ const HALF_WIDTH = 0.34;
13
+ const THICKNESS = 0.08;
14
+ const CELL_ASPECT = 0.5;
15
+ const CAM = 8.6;
16
+ const FOCAL = 5.1;
17
+ const KEY = norm([-0.58, 0.78, 0.24]);
18
+ const FILL = norm([0.48, 0.12, 0.86]);
19
+ const VIEW = [0, 0.12, 1];
20
+ function clamp(value, lo, hi) {
21
+ return Math.max(lo, Math.min(hi, value));
22
+ }
23
+ function lerp(a, b, t) {
24
+ return a + (b - a) * t;
25
+ }
26
+ function norm(v) {
27
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
28
+ return [v[0] / length, v[1] / length, v[2] / length];
29
+ }
30
+ function cross(a, b) {
31
+ return [
32
+ a[1] * b[2] - a[2] * b[1],
33
+ a[2] * b[0] - a[0] * b[2],
34
+ a[0] * b[1] - a[1] * b[0],
35
+ ];
36
+ }
37
+ function dot(a, b) {
38
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
39
+ }
40
+ /** Brand gradient along the band: blue → purple → gold, matching sisu-mark. */
41
+ function mobiusRgb(t) {
42
+ const u = ((t / (Math.PI * 2)) % 1 + 1) % 1;
43
+ if (u < 0.5) {
44
+ const k = u / 0.5;
45
+ return [
46
+ Math.round(lerp(37, 124, k)),
47
+ Math.round(lerp(99, 58, k)),
48
+ Math.round(lerp(235, 237, k)),
49
+ ];
50
+ }
51
+ const k = (u - 0.5) / 0.5;
52
+ return [
53
+ Math.round(lerp(124, 217, k)),
54
+ Math.round(lerp(58, 119, k)),
55
+ Math.round(lerp(237, 6, k)),
56
+ ];
57
+ }
58
+ function ansiRgb(r, g, b) {
59
+ return `\x1b[38;2;${r};${g};${b}m`;
60
+ }
61
+ function rotateX(y, z, angle) {
62
+ const c = Math.cos(angle);
63
+ const s = Math.sin(angle);
64
+ return [y * c - z * s, y * s + z * c];
65
+ }
66
+ function rotateY(x, z, angle) {
67
+ const c = Math.cos(angle);
68
+ const s = Math.sin(angle);
69
+ return [x * c + z * s, -x * s + z * c];
70
+ }
71
+ function rotateZ(x, y, angle) {
72
+ const c = Math.cos(angle);
73
+ const s = Math.sin(angle);
74
+ return [x * c - y * s, x * s + y * c];
75
+ }
76
+ function transform(x, y, z, tilt, spin) {
77
+ // Spin around the ring axis so the half-twist travels; then a fixed 3/4 view.
78
+ ;
79
+ [x, y] = rotateZ(x, y, spin);
80
+ [y, z] = rotateX(y, z, tilt);
81
+ [x, z] = rotateY(x, z, 0.32);
82
+ return [x, y, z];
83
+ }
84
+ /**
85
+ * Standard Möbius strip: a ring of radius R with a half-twist in the width.
86
+ * After one full trip around θ, the normal flips — one-sided surface.
87
+ */
88
+ function mobiusPoint(theta, width, radius = RADIUS, halfWidth = HALF_WIDTH) {
89
+ const twist = theta / 2;
90
+ const ring = radius + width * halfWidth * Math.cos(twist);
91
+ return [
92
+ ring * Math.cos(theta),
93
+ ring * Math.sin(theta),
94
+ width * halfWidth * Math.sin(twist),
95
+ ];
96
+ }
97
+ function mobiusNormal(theta, width, radius = RADIUS, halfWidth = HALF_WIDTH) {
98
+ const twist = theta / 2;
99
+ const c = Math.cos(theta);
100
+ const s = Math.sin(theta);
101
+ const c2 = Math.cos(twist);
102
+ const s2 = Math.sin(twist);
103
+ const ring = radius + width * halfWidth * c2;
104
+ const dTheta = [
105
+ -ring * s - width * halfWidth * 0.5 * s2 * c,
106
+ ring * c - width * halfWidth * 0.5 * s2 * s,
107
+ width * halfWidth * 0.5 * c2,
108
+ ];
109
+ const dWidth = [
110
+ halfWidth * c2 * c,
111
+ halfWidth * c2 * s,
112
+ halfWidth * s2,
113
+ ];
114
+ return norm(cross(dTheta, dWidth));
115
+ }
116
+ function shadeChar(shade) {
117
+ return SHADE_RAMP[Math.round(clamp(shade, 0, 1) * (SHADE_RAMP.length - 1))] || '@';
118
+ }
119
+ function renderMobiusFrame(options = {}) {
120
+ const cols = Math.max(28, options.cols ?? 64);
121
+ const rows = Math.max(10, options.rows ?? 16);
122
+ const phase = options.phase ?? 0;
123
+ const color = options.color !== false;
124
+ const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
125
+ const tilt = 0.88;
126
+ const samples = [];
127
+ const thetaSteps = 580;
128
+ for (let i = 0; i < thetaSteps; i += 1) {
129
+ const theta = (i / thetaSteps) * Math.PI * 2;
130
+ for (let j = -4; j <= 4; j += 1) {
131
+ const width = j / 4;
132
+ const base = mobiusPoint(theta, width);
133
+ const n0 = mobiusNormal(theta, width);
134
+ // Faces of the thin solid, plus the mid-surface; skip the interior fill.
135
+ const thicks = j === -4 || j === 4 ? [-1, 0, 1] : [-1, 1];
136
+ for (const k of thicks) {
137
+ const thick = k;
138
+ let x = base[0] + n0[0] * thick * THICKNESS;
139
+ let y = base[1] + n0[1] * thick * THICKNESS;
140
+ let z = base[2] + n0[2] * thick * THICKNESS;
141
+ let nx = n0[0];
142
+ let ny = n0[1];
143
+ let nz = n0[2];
144
+ if (thick < 0) {
145
+ nx = -nx;
146
+ ny = -ny;
147
+ nz = -nz;
148
+ }
149
+ ;
150
+ [x, y, z] = transform(x, y, z, tilt, phase);
151
+ [nx, ny, nz] = transform(nx, ny, nz, tilt, phase);
152
+ let normal = [nx, ny, nz];
153
+ const facing = dot(normal, VIEW);
154
+ const back = facing < 0;
155
+ if (back)
156
+ normal = [-normal[0], -normal[1], -normal[2]];
157
+ const depth = CAM - z;
158
+ if (depth < 0.35)
159
+ continue;
160
+ const px = (x * FOCAL) / depth;
161
+ const py = (y * FOCAL) / depth;
162
+ const lambert = Math.max(0, dot(normal, KEY));
163
+ const fill = Math.max(0, dot(normal, FILL)) * 0.2;
164
+ const camFill = Math.max(0, dot(normal, VIEW)) * 0.22;
165
+ const half = norm([KEY[0] + VIEW[0], KEY[1] + VIEW[1], KEY[2] + VIEW[2]]);
166
+ const spec = Math.pow(Math.max(0, dot(normal, half)), 28);
167
+ const edge = Math.max(Math.abs(width), Math.abs(thick));
168
+ const rim = Math.pow(1 - Math.abs(dot(normal, VIEW)), 2.2) * (0.1 + 0.5 * Math.pow(edge, 2));
169
+ const ambient = back ? 0.09 : 0.18;
170
+ const lit = ambient + lambert * (back ? 0.3 : 0.74) + fill + camFill + spec * 0.5 + rim;
171
+ const shade = clamp(Math.pow(lit, 1.2), 0.07, 1);
172
+ samples.push({
173
+ px,
174
+ py,
175
+ depth,
176
+ shade,
177
+ spec,
178
+ t: theta + (back ? Math.PI : 0),
179
+ });
180
+ }
181
+ }
182
+ }
183
+ let minX = Infinity;
184
+ let maxX = -Infinity;
185
+ let minY = Infinity;
186
+ let maxY = -Infinity;
187
+ let minD = Infinity;
188
+ let maxD = -Infinity;
189
+ for (const sample of samples) {
190
+ const sx = sample.px;
191
+ const sy = sample.py * CELL_ASPECT;
192
+ if (sx < minX)
193
+ minX = sx;
194
+ if (sx > maxX)
195
+ maxX = sx;
196
+ if (sy < minY)
197
+ minY = sy;
198
+ if (sy > maxY)
199
+ maxY = sy;
200
+ if (sample.depth < minD)
201
+ minD = sample.depth;
202
+ if (sample.depth > maxD)
203
+ maxD = sample.depth;
204
+ }
205
+ const spanX = Math.max(0.001, maxX - minX);
206
+ const spanY = Math.max(0.001, maxY - minY);
207
+ const scale = Math.min((cols - 4) / spanX, (rows - 2) / spanY);
208
+ const midX = (minX + maxX) / 2;
209
+ const midY = (minY + maxY) / 2;
210
+ const depthSpan = Math.max(0.001, maxD - minD);
211
+ for (const sample of samples) {
212
+ const fog = 0.4 + 0.6 * (1 - (sample.depth - minD) / depthSpan);
213
+ const col = Math.round((sample.px - midX) * scale + (cols - 1) / 2);
214
+ const row = Math.round(-((sample.py * CELL_ASPECT) - midY) * scale + (rows - 1) / 2);
215
+ if (col < 0 || col >= cols || row < 0 || row >= rows)
216
+ continue;
217
+ const prev = grid[row][col];
218
+ if (!prev || sample.depth < prev.z) {
219
+ grid[row][col] = {
220
+ z: sample.depth,
221
+ shade: clamp(sample.shade * fog, 0.06, 1),
222
+ t: sample.t,
223
+ spec: sample.spec * fog,
224
+ };
225
+ }
226
+ }
227
+ return grid.map((line) => line.map((cell) => {
228
+ if (!cell)
229
+ return ' ';
230
+ const ch = shadeChar(cell.shade);
231
+ if (!color)
232
+ return ch;
233
+ let [r, g, b] = mobiusRgb(cell.t);
234
+ const lift = 0.18 + cell.shade * 0.95 + cell.spec * 0.7;
235
+ r = clamp(Math.round(r * lift), 0, 255);
236
+ g = clamp(Math.round(g * lift), 0, 255);
237
+ b = clamp(Math.round(b * lift), 0, 255);
238
+ return `${ansiRgb(r, g, b)}${ch}${RESET}`;
239
+ }).join('')).join('\n');
240
+ }
241
+ function mobiusFrameHeight(columns = 80) {
242
+ return columns < 56 ? 13 : 20;
243
+ }
244
+ function mobiusFrameWidth(columns = 80) {
245
+ return columns < 56 ? 44 : Math.min(76, Math.max(56, columns - 4));
246
+ }
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatChromeStatus = formatChromeStatus;
4
+ exports.chromeShortQuota = chromeShortQuota;
5
+ exports.runPager = runPager;
6
+ const store_1 = require("../store");
7
+ const input_1 = require("./input");
8
+ const model_1 = require("./model");
9
+ const history_1 = require("./history");
10
+ const render_1 = require("./render");
11
+ const ALT_ENTER = '\x1b[?1049h\x1b[?25l';
12
+ const ALT_LEAVE = '\x1b[?1049l\x1b[?25h';
13
+ const CLEAR_HOME = '\x1b[2J\x1b[H';
14
+ function formatChromeStatus(email, quota, conversationId) {
15
+ const parts = [
16
+ (email || '').trim(),
17
+ (quota || '').trim() || 'quota unavailable',
18
+ conversationId || 'new',
19
+ 'client=tui',
20
+ ].filter((part) => part.length > 0);
21
+ return parts.join(' · ');
22
+ }
23
+ /** Short chrome fragment: `quota unlimited`, first `quota N pts`, or `quota unavailable`. */
24
+ function chromeShortQuota(full) {
25
+ const text = (full || '').trim();
26
+ if (!text)
27
+ return 'quota unavailable';
28
+ const first = text.split(' · ')[0].trim();
29
+ if (first === 'quota unlimited')
30
+ return 'quota unlimited';
31
+ if (/^quota\s+\d+\s+pts$/.test(first))
32
+ return first;
33
+ if (first === 'quota unavailable' || first.startsWith('quota unavailable'))
34
+ return 'quota unavailable';
35
+ return 'quota unavailable';
36
+ }
37
+ let nextAppEntryId = 1;
38
+ function pushEntry(state, kind, text) {
39
+ const folded = kind === 'tool' && text.split('\n').length > 8;
40
+ const entry = { id: `p${nextAppEntryId}`, kind, text, folded };
41
+ nextAppEntryId += 1;
42
+ const entries = [...state.entries, entry];
43
+ return { ...state, entries, selected: entries.length - 1 };
44
+ }
45
+ function clearDraft(state) {
46
+ return { ...state, draft: '', slashOpen: false, slashIndex: 0 };
47
+ }
48
+ function submittedText(state) {
49
+ const raw = state.draft.trim();
50
+ if (state.slashOpen && !/\s/.test(raw)) {
51
+ const items = (0, model_1.filterSlash)(state.draft);
52
+ const picked = items[state.slashIndex];
53
+ if (picked)
54
+ return picked.name;
55
+ }
56
+ return raw;
57
+ }
58
+ function commandHead(text) {
59
+ const token = text.split(/\s+/, 1)[0] || text;
60
+ if (token === '/exit')
61
+ return '/quit';
62
+ if (token === '/clear')
63
+ return '/new';
64
+ if (token === '/history')
65
+ return '/resume';
66
+ return token;
67
+ }
68
+ async function resolveText(value) {
69
+ return value;
70
+ }
71
+ async function runPager(io, transport, options = {}) {
72
+ const cols = options.columns ?? io.columns;
73
+ const rows = options.rows ?? io.rows;
74
+ let theme = options.theme ?? 'dark';
75
+ let quotaLine = 'quota unavailable';
76
+ const withChrome = (next) => ({
77
+ ...next,
78
+ statusLine: formatChromeStatus(options.email, quotaLine, next.conversationId),
79
+ });
80
+ const refreshQuota = async () => {
81
+ if (!options.quota)
82
+ return;
83
+ try {
84
+ quotaLine = chromeShortQuota((await resolveText(options.quota())).trim() || 'quota unavailable');
85
+ }
86
+ catch {
87
+ quotaLine = 'quota unavailable';
88
+ }
89
+ };
90
+ let state = withChrome((0, model_1.createPagerState)());
91
+ let rest = '';
92
+ let newConversation = false;
93
+ let pickMode = false;
94
+ const pickableIds = new Map();
95
+ let running = true;
96
+ let processing = false;
97
+ const pending = [];
98
+ let unsubscribe = () => undefined;
99
+ const paint = () => {
100
+ if (!running)
101
+ return;
102
+ io.write(`${CLEAR_HOME}${(0, render_1.renderPager)(state, cols, rows, theme)}`);
103
+ };
104
+ let removeTerm = () => undefined;
105
+ let settle;
106
+ let abortWait;
107
+ const finish = (code) => {
108
+ if (!running)
109
+ return;
110
+ running = false;
111
+ pending.length = 0;
112
+ unsubscribe();
113
+ abortWait?.();
114
+ settle?.(code);
115
+ };
116
+ try {
117
+ io.enterRaw();
118
+ io.write(ALT_ENTER);
119
+ const onTerm = () => finish(0);
120
+ process.on('SIGTERM', onTerm);
121
+ removeTerm = () => {
122
+ process.removeListener('SIGTERM', onTerm);
123
+ removeTerm = () => undefined;
124
+ };
125
+ paint();
126
+ await Promise.race([
127
+ refreshQuota(),
128
+ new Promise((resolve) => { abortWait = resolve; }),
129
+ ]);
130
+ if (!running)
131
+ return 0;
132
+ state = withChrome(state);
133
+ return await new Promise((resolve, reject) => {
134
+ settle = resolve;
135
+ paint();
136
+ const fail = (err) => {
137
+ if (!running)
138
+ return;
139
+ running = false;
140
+ pending.length = 0;
141
+ unsubscribe();
142
+ reject(err);
143
+ };
144
+ const persistOpen = (boundId) => {
145
+ (0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_conversation_id: boundId });
146
+ };
147
+ const openConversation = async (id) => {
148
+ pickMode = false;
149
+ newConversation = false;
150
+ persistOpen(id);
151
+ state = withChrome({ ...state, conversationId: id, entries: [], selected: 0 });
152
+ try {
153
+ const row = await transport.getConversation(id);
154
+ if (!running)
155
+ return;
156
+ const bound = row.id || id;
157
+ persistOpen(bound);
158
+ const mapped = (0, history_1.entriesFromMessages)(row.messages);
159
+ const seeded = mapped.entries.map((entry) => {
160
+ const next = { ...entry, id: `p${nextAppEntryId}` };
161
+ nextAppEntryId += 1;
162
+ return next;
163
+ });
164
+ const entries = mapped.truncated
165
+ ? [...seeded, { id: `p${nextAppEntryId++}`, kind: 'status', text: 'showing last 200 messages', folded: false }]
166
+ : seeded;
167
+ state = withChrome({
168
+ ...state,
169
+ conversationId: bound,
170
+ entries,
171
+ selected: Math.max(0, entries.length - 1),
172
+ });
173
+ }
174
+ catch (err) {
175
+ if (!running)
176
+ return;
177
+ state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
178
+ state = withChrome(state);
179
+ }
180
+ };
181
+ const submitSlash = async (text) => {
182
+ const head = commandHead(text);
183
+ state = clearDraft(state);
184
+ if (head === '/quit') {
185
+ finish(0);
186
+ return;
187
+ }
188
+ if (head === '/new') {
189
+ pickMode = false;
190
+ pickableIds.clear();
191
+ state = withChrome({
192
+ ...state,
193
+ entries: [],
194
+ selected: 0,
195
+ conversationId: '',
196
+ });
197
+ newConversation = true;
198
+ paint();
199
+ return;
200
+ }
201
+ if (head === '/resume') {
202
+ pickMode = false;
203
+ pickableIds.clear();
204
+ try {
205
+ const rowsList = await transport.listConversations();
206
+ if (!running)
207
+ return;
208
+ if (!rowsList.length) {
209
+ state = pushEntry(state, 'status', 'no conversations');
210
+ }
211
+ else {
212
+ pickMode = true;
213
+ // API is last_activity_at DESC (newest first). Append oldest-first so
214
+ // the newest row is last, selected, and visible above the prompt.
215
+ const oldestFirst = rowsList.slice().reverse();
216
+ oldestFirst.forEach((row, index) => {
217
+ const client = row.client ? ` [${row.client}]` : '';
218
+ state = pushEntry(state, 'status', `${index + 1}. ${row.id} ${row.title}${client}`);
219
+ const entry = state.entries[state.entries.length - 1];
220
+ if (row.id)
221
+ pickableIds.set(entry.id, row.id);
222
+ });
223
+ }
224
+ }
225
+ catch (err) {
226
+ if (!running)
227
+ return;
228
+ state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
229
+ }
230
+ paint();
231
+ return;
232
+ }
233
+ if (head === '/open') {
234
+ const id = text.slice('/open'.length).trim();
235
+ if (!id) {
236
+ state = pushEntry(state, 'status', 'usage: /open <id>');
237
+ }
238
+ else {
239
+ await openConversation(id);
240
+ }
241
+ paint();
242
+ return;
243
+ }
244
+ if (head === '/status') {
245
+ const body = options.status ? await resolveText(options.status()) : 'not wired';
246
+ if (!running)
247
+ return;
248
+ state = pushEntry(state, 'status', body);
249
+ paint();
250
+ return;
251
+ }
252
+ if (head === '/ls') {
253
+ const body = options.ls ? await resolveText(options.ls()) : 'not wired';
254
+ if (!running)
255
+ return;
256
+ state = pushEntry(state, 'status', body);
257
+ paint();
258
+ return;
259
+ }
260
+ if (head === '/training') {
261
+ const arg = text.slice('/training'.length).trim();
262
+ if (!options.training) {
263
+ state = pushEntry(state, 'status', 'not wired');
264
+ }
265
+ else if (arg !== 'on' && arg !== 'off') {
266
+ state = pushEntry(state, 'status', 'usage: /training on|off');
267
+ }
268
+ else {
269
+ const body = await resolveText(options.training(arg === 'on'));
270
+ if (!running)
271
+ return;
272
+ state = pushEntry(state, 'status', body);
273
+ }
274
+ paint();
275
+ return;
276
+ }
277
+ if (head === '/theme') {
278
+ theme = theme === 'dark' ? 'light' : 'dark';
279
+ state = pushEntry(state, 'status', `theme ${theme}`);
280
+ paint();
281
+ return;
282
+ }
283
+ if (head === '/help') {
284
+ state = pushEntry(state, 'status', model_1.SLASH_COMMANDS.map((item) => `${item.name} ${item.hint}`).join('\n'));
285
+ paint();
286
+ return;
287
+ }
288
+ state = pushEntry(state, 'status', `unknown command ${text}`);
289
+ paint();
290
+ };
291
+ const sendTurn = async (prompt) => {
292
+ state = pushEntry(state, 'user', prompt);
293
+ state = (0, model_1.startAssistant)(state);
294
+ state = clearDraft(state);
295
+ paint();
296
+ const sendOptions = {
297
+ conversationId: newConversation ? undefined : state.conversationId || undefined,
298
+ newConversation,
299
+ };
300
+ try {
301
+ const gen = transport.send(prompt, sendOptions);
302
+ let step = await gen.next();
303
+ while (running && !step.done) {
304
+ const event = step.value;
305
+ if (event.type === 'bound' && event.text) {
306
+ state = withChrome({ ...state, conversationId: event.text });
307
+ newConversation = false;
308
+ }
309
+ else if (event.type === 'text' && event.text) {
310
+ state = (0, model_1.appendText)(state, event.text);
311
+ }
312
+ else if (event.type === 'error') {
313
+ state = pushEntry(state, 'status', event.text || 'stream error');
314
+ }
315
+ else if (event.type === 'status' && event.text) {
316
+ state = pushEntry(state, 'status', event.text);
317
+ }
318
+ else if (event.type === 'tool' && event.text) {
319
+ const folded = event.text.split('\n').length > 8;
320
+ const entry = { id: `p${nextAppEntryId}`, kind: 'tool', text: event.text, folded };
321
+ nextAppEntryId += 1;
322
+ state = (0, model_1.insertToolBeforeLiveAssistant)(state, entry);
323
+ }
324
+ paint();
325
+ if (!running)
326
+ return;
327
+ step = await gen.next();
328
+ }
329
+ if (!running || !step.done)
330
+ return;
331
+ const conversationId = step.value.conversationId;
332
+ if (conversationId) {
333
+ state = withChrome({ ...state, conversationId });
334
+ newConversation = false;
335
+ }
336
+ await refreshQuota();
337
+ state = withChrome(state);
338
+ paint();
339
+ }
340
+ catch (err) {
341
+ if (!running)
342
+ return;
343
+ state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
344
+ await refreshQuota();
345
+ state = withChrome(state);
346
+ paint();
347
+ }
348
+ };
349
+ const handleKey = async (key) => {
350
+ if (!running)
351
+ return;
352
+ if (key.type === 'escape' && !state.slashOpen && state.draft === '') {
353
+ finish(0);
354
+ return;
355
+ }
356
+ if (key.type === 'enter') {
357
+ const text = submittedText(state);
358
+ if (!text) {
359
+ const selected = state.entries[state.selected];
360
+ const picked = pickMode && selected ? pickableIds.get(selected.id) : undefined;
361
+ if (picked) {
362
+ await openConversation(picked);
363
+ paint();
364
+ return;
365
+ }
366
+ state = (0, model_1.applyKey)(state, key);
367
+ paint();
368
+ return;
369
+ }
370
+ if (text.startsWith('/')) {
371
+ await submitSlash(text);
372
+ return;
373
+ }
374
+ await sendTurn(text);
375
+ return;
376
+ }
377
+ state = (0, model_1.applyKey)(state, key);
378
+ paint();
379
+ };
380
+ const drain = async () => {
381
+ if (processing)
382
+ return;
383
+ processing = true;
384
+ try {
385
+ while (running && pending.length) {
386
+ const key = pending.shift();
387
+ if (!key)
388
+ break;
389
+ await handleKey(key);
390
+ }
391
+ }
392
+ catch (err) {
393
+ fail(err);
394
+ }
395
+ finally {
396
+ processing = false;
397
+ if (running && pending.length)
398
+ void drain();
399
+ }
400
+ };
401
+ unsubscribe = io.onData((chunk) => {
402
+ const decoded = (0, input_1.decodeKeys)(rest + chunk);
403
+ rest = decoded.rest;
404
+ pending.push(...decoded.keys);
405
+ void drain();
406
+ });
407
+ options.ready?.();
408
+ });
409
+ }
410
+ finally {
411
+ running = false;
412
+ removeTerm();
413
+ try {
414
+ io.write(ALT_LEAVE);
415
+ }
416
+ finally {
417
+ io.leaveRaw();
418
+ }
419
+ }
420
+ }