@inneranimalmedia/agentsam-sdk 1.5.0 → 1.6.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.
@@ -0,0 +1,290 @@
1
+ /**
2
+ * @file src/ui/splash-xterm.js
3
+ * @description In-app splash renderer for xterm.js panels.
4
+ *
5
+ * Same choreography as splash.js (CLI) but writes to an xterm Terminal
6
+ * instance instead of process.stdout. Used by XTermShell.tsx.
7
+ *
8
+ * Usage in XTermShell.tsx:
9
+ * import { runXtermSplash } from '@inneranimalmedia/agentsam-sdk/ui/splash-xterm';
10
+ * import { getXtermOptions } from '@inneranimalmedia/agentsam-sdk/ui/theme';
11
+ *
12
+ * const targetType = connection?.target_type ?? 'platform_vm';
13
+ * const term = new Terminal({
14
+ * ...getXtermOptions(targetType),
15
+ * cols: dimensions.cols,
16
+ * rows: dimensions.rows,
17
+ * });
18
+ *
19
+ * // After term.open(containerRef.current):
20
+ * const stopFlicker = await runXtermSplash(term, {
21
+ * apiBase: 'https://inneranimalmedia.com',
22
+ * workspaceId: activeWorkspace?.id,
23
+ * authToken: session?.token,
24
+ * targetType,
25
+ * skipArt: !isFirstSession,
26
+ * });
27
+ *
28
+ * // On cleanup:
29
+ * return () => { stopFlicker(); term.dispose(); };
30
+ */
31
+
32
+ import { PALETTE, getLaneTheme } from './theme.js';
33
+ import { probeAll } from './splash.js';
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // xterm write helpers
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ const ESC = '\x1b';
40
+ const CSI = `${ESC}[`;
41
+
42
+ const X = {
43
+ reset: `${CSI}0m`,
44
+ bold: `${CSI}1m`,
45
+ hide: `${CSI}?25l`,
46
+ show: `${CSI}?25h`,
47
+ clearLine: `${CSI}2K\r`,
48
+ up: (n = 1) => `${CSI}${n}A`,
49
+ fg: (hex) => {
50
+ const h = hex.replace('#', '');
51
+ const r = parseInt(h.slice(0, 2), 16);
52
+ const g = parseInt(h.slice(2, 4), 16);
53
+ const b = parseInt(h.slice(4, 6), 16);
54
+ return `${CSI}38;2;${r};${g};${b}m`;
55
+ },
56
+ };
57
+
58
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // COLOR BUILDER (per-lane)
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ function buildColors() {
65
+ const R = X.reset;
66
+ const B = X.bold;
67
+ return {
68
+ R, B,
69
+ TEAL: X.fg(PALETTE.teal300),
70
+ AMBER: X.fg(PALETTE.amber300),
71
+ STONE: X.fg('#3a3530'),
72
+ STONE2: X.fg('#2a2520'),
73
+ VINE: X.fg('#1a4a2a'),
74
+ VINE2: X.fg('#0f6b3a'),
75
+ FIRE1: X.fg('#f97316'),
76
+ FIRE2: X.fg('#ef4444'),
77
+ FIREY: X.fg('#fef08a'),
78
+ GRILL: X.fg('#1e2830'),
79
+ GRILL2: X.fg('#2d3d4a'),
80
+ PELT: X.fg('#8b6f5a'),
81
+ DIM: X.fg('#444444'),
82
+ GHOST: X.fg('#888888'),
83
+ WHT: X.fg('#e2e8f0'),
84
+ };
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // SCENE DATA
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ function gorillaRows(C) {
92
+ const { R, GRILL, GRILL2, PELT, DIM } = C;
93
+ return [
94
+ ` ${DIM}░░▒${R}${GRILL2}▓▓▓▓▓▓▓▓${R}${DIM}▒░░${R}`,
95
+ ` ${DIM}░${R}${GRILL2}▒▓▓${R}${GRILL}█████████████${R}${GRILL2}▓▓▒${R}${DIM}░${R}`,
96
+ ` ${GRILL2}▒▓${R}${GRILL}███${R}${GRILL2}▒▒${R}${GRILL}████████${R}${GRILL2}▒▒${R}${GRILL}███${R}${GRILL2}▓▒${R}`,
97
+ ` ${GRILL2}▓${R}${GRILL}████${R} ${DIM}▒▒▒▒▒▒▒▒${R} ${GRILL}████${R}${GRILL2}▓${R}`,
98
+ ` ${GRILL}▓███${R}${GRILL2}▒${R} ${PELT}██${R}${DIM}▒${R}${PELT}████${R}${DIM}▒${R}${PELT}██${R} ${GRILL2}▒${R}${GRILL}███▓${R}`,
99
+ ` ${GRILL}████${R} ${PELT}███${R}${DIM}▒▒▒▒${R}${PELT}███${R} ${GRILL}████${R}`,
100
+ ` ${GRILL}████${R}${GRILL2}▒${R} ${PELT}██████████${R} ${GRILL2}▒${R}${GRILL}████${R}`,
101
+ ` ${GRILL}████${R} ${DIM}▒▒▒▒▒▒▒▒▒▒${R} ${GRILL}████${R}`,
102
+ ` ${GRILL2}▒${R}${GRILL}████${R}${GRILL2}▓▓▓${R}${GRILL}████████████${R}${GRILL2}▓▓▓${R}${GRILL}████${R}${GRILL2}▒${R}`,
103
+ ` ${GRILL}██████████████████████████${R}`,
104
+ ` ${DIM}▒▒${R}${GRILL}████████${R}${GRILL2}▒▒▒▒▒▒▒▒▒▒▒▒${R}${GRILL}████████${R}${DIM}▒▒${R}`,
105
+ ` ${PELT}▓${R}${GRILL}██████████${R}${DIM}░░░░░░░░░░░░░░${R}${GRILL}██████████${R}${PELT}▓${R}`,
106
+ ];
107
+ }
108
+
109
+ function titleBoxLines(C) {
110
+ const { R, B, TEAL, AMBER } = C;
111
+ const wide = 26;
112
+ const h = `${TEAL}═${R}`;
113
+ return [
114
+ `${TEAL}╔${R}${h.repeat(wide)}${TEAL}╗${R}`,
115
+ `${TEAL}║${R} ${B}${AMBER}INNERANIMAL MEDIA${R} ${TEAL}║${R}`,
116
+ `${TEAL}║${R} ${TEAL}inneranimalmedia.com${R} ${TEAL}║${R}`,
117
+ `${TEAL}╚${R}${h.repeat(wide)}${TEAL}╝${R}`,
118
+ ];
119
+ }
120
+
121
+ function ladderRows(C) {
122
+ const { R, TEAL } = C;
123
+ return [
124
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
125
+ ` ${TEAL}╠═══╣${R}`,
126
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
127
+ ` ${TEAL}╠═══╣${R}`,
128
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
129
+ ` ${TEAL}╠═══╣${R}`,
130
+ ` ${TEAL}║${R} ${TEAL}║${R}`,
131
+ ];
132
+ }
133
+
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ // HUD RENDER
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+
138
+ const HUD_ITEMS = [
139
+ { key: 'workspace', icon: '⊞', label: 'Workspace' },
140
+ { key: 'runtime', icon: '▣', label: 'Runtime' },
141
+ { key: 'tunnel', icon: '⟁', label: 'Tunnel' },
142
+ { key: 'agent', icon: '⬡', label: 'Agent' },
143
+ ];
144
+
145
+ const READY_LABELS = {
146
+ workspace: 'active',
147
+ runtime: 'ready',
148
+ tunnel: 'connected',
149
+ agent: 'online',
150
+ };
151
+
152
+ function renderHUDLine(states, C) {
153
+ const { R, B, TEAL, GHOST, WHT, DIM } = C;
154
+ return HUD_ITEMS.map((item, i) => {
155
+ const state = states[item.key] || 'checking';
156
+ let stateStr;
157
+ if (state === 'ready') {
158
+ stateStr = `${TEAL}● ${READY_LABELS[item.key]}${R}`;
159
+ } else if (state === 'checking') {
160
+ stateStr = `${GHOST}◌ checking...${R}`;
161
+ } else if (state === 'error') {
162
+ stateStr = `${X.fg('#ef4444')}✗ error${R}`;
163
+ } else {
164
+ stateStr = `${X.fg('#666666')}○ offline${R}`;
165
+ }
166
+ const div = i < HUD_ITEMS.length - 1 ? ` ${DIM}│${R} ` : '';
167
+ return `${GHOST}${item.icon}${R} ${B}${WHT}${item.label}${R} ${stateStr}${div}`;
168
+ }).join('');
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // MAIN XTERM SPLASH
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * @param {import('@xterm/xterm').Terminal} term
177
+ * @param {object} opts
178
+ * @param {string} opts.apiBase
179
+ * @param {string} opts.workspaceId
180
+ * @param {string} opts.authToken
181
+ * @param {'user_hosted_tunnel'|'platform_vm'|'sandbox'} opts.targetType
182
+ * @param {boolean} opts.skipArt
183
+ * @returns {Promise<() => void>} stopFlicker — call on unmount
184
+ */
185
+ export async function runXtermSplash(term, {
186
+ apiBase = '',
187
+ workspaceId = '',
188
+ authToken = '',
189
+ targetType = 'platform_vm',
190
+ skipArt = false,
191
+ } = {}) {
192
+
193
+ const lane = getLaneTheme(targetType);
194
+ const C = buildColors();
195
+ const { R, B, TEAL, AMBER, GHOST, WHT, DIM, STONE, STONE2, FIRE1, FIRE2 } = C;
196
+ const PRIMARY = X.fg(lane.colors.primary);
197
+ const W = term.cols || 80;
198
+
199
+ const w = (s) => term.write(s);
200
+ const wl = (s = '') => term.write(s + '\r\n');
201
+
202
+ w(X.hide);
203
+ w(`${ESC}c`); // full reset
204
+ await sleep(60);
205
+
206
+ // cd verb flash
207
+ w(` ${GHOST}$ ${R}${AMBER}cd ~/inneranimalmedia${R}`);
208
+ await sleep(340);
209
+ w(`\r${X.clearLine}`);
210
+ await sleep(60);
211
+
212
+ if (!skipArt) {
213
+ wl();
214
+
215
+ const rows = gorillaRows(C);
216
+ const title = titleBoxLines(C);
217
+ const TITLE_START = 3;
218
+
219
+ for (let i = 0; i < rows.length; i++) {
220
+ const titleIdx = i - TITLE_START;
221
+ const titlePart = (titleIdx >= 0 && titleIdx < title.length)
222
+ ? ' ' + title[titleIdx]
223
+ : '';
224
+ wl(` ${rows[i]}${titlePart}`);
225
+ await sleep(24);
226
+ }
227
+
228
+ wl();
229
+
230
+ // Torches
231
+ w(` ${FIRE1} ▓▓▓${R}`);
232
+ await sleep(90);
233
+ wl(`${' '.repeat(Math.max(0, W - 14))}${FIRE2}▓▓▓${R}`);
234
+ await sleep(30);
235
+
236
+ // Ledge
237
+ w(' ');
238
+ for (let i = 0; i < W - 4; i++) {
239
+ w((i % 2 === 0) ? `${STONE}█${R}` : `${STONE2}▓${R}`);
240
+ await sleep(4);
241
+ }
242
+ wl();
243
+ wl(` ${DIM}${'╌'.repeat(W - 4)}${R}`);
244
+ await sleep(50);
245
+
246
+ // Ladder
247
+ for (const rung of ladderRows(C)) {
248
+ wl(rung);
249
+ await sleep(30);
250
+ }
251
+ }
252
+
253
+ wl();
254
+
255
+ // Start prompt
256
+ await sleep(skipArt ? 0 : 70);
257
+ wl(` ${B}${AMBER}Start ${PRIMARY}▸${R}`);
258
+ await sleep(40);
259
+ wl(` ${GHOST}Type a command to begin.${R}`);
260
+ await sleep(skipArt ? 0 : 70);
261
+ wl();
262
+
263
+ // HUD — fire probes in parallel
264
+ const probePromise = probeAll({ apiBase, workspaceId, authToken });
265
+ const initStates = { workspace: 'checking', runtime: 'checking', tunnel: 'checking', agent: 'checking' };
266
+
267
+ wl(` ${DIM}${'─'.repeat(W - 4)}${R}`);
268
+ wl();
269
+ w(` ${renderHUDLine(initStates, C)}`);
270
+ wl(); wl();
271
+ wl(` ${DIM}${'─'.repeat(W - 4)}${R}`);
272
+ wl();
273
+
274
+ // Update HUD in-place when probes resolve
275
+ const LINES_BELOW = 4;
276
+ probePromise.then(resolved => {
277
+ w(X.up(LINES_BELOW));
278
+ w(`\r${X.clearLine}`);
279
+ w(` ${renderHUDLine(resolved, C)}`);
280
+ w('\r\n'.repeat(LINES_BELOW));
281
+ w(X.show);
282
+ w(` ${PRIMARY}>${R} `);
283
+ });
284
+
285
+ // Torch flicker is handled via CSS animation on the panel wrapper
286
+ // (terminal-lanes.css) — xterm.js decoration API in Phase 2.
287
+ return () => {};
288
+ }
289
+
290
+ export default { runXtermSplash };