aicodeman 1.9.6 → 1.9.7

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.
Files changed (126) hide show
  1. package/dist/config/dependency-registry.d.ts.map +1 -1
  2. package/dist/config/dependency-registry.js +8 -0
  3. package/dist/config/dependency-registry.js.map +1 -1
  4. package/dist/docker-hosts.d.ts.map +1 -1
  5. package/dist/docker-hosts.js +1 -0
  6. package/dist/docker-hosts.js.map +1 -1
  7. package/dist/mux-interface.d.ts +3 -1
  8. package/dist/mux-interface.d.ts.map +1 -1
  9. package/dist/remote-hosts.d.ts.map +1 -1
  10. package/dist/remote-hosts.js +1 -0
  11. package/dist/remote-hosts.js.map +1 -1
  12. package/dist/session.d.ts +6 -1
  13. package/dist/session.d.ts.map +1 -1
  14. package/dist/session.js +18 -3
  15. package/dist/session.js.map +1 -1
  16. package/dist/tmux-manager.d.ts +2 -1
  17. package/dist/tmux-manager.d.ts.map +1 -1
  18. package/dist/tmux-manager.js +46 -5
  19. package/dist/tmux-manager.js.map +1 -1
  20. package/dist/types/session.d.ts +16 -4
  21. package/dist/types/session.d.ts.map +1 -1
  22. package/dist/types/session.js +2 -1
  23. package/dist/types/session.js.map +1 -1
  24. package/dist/utils/antigravity-cli-resolver.d.ts +20 -0
  25. package/dist/utils/antigravity-cli-resolver.d.ts.map +1 -0
  26. package/dist/utils/antigravity-cli-resolver.js +60 -0
  27. package/dist/utils/antigravity-cli-resolver.js.map +1 -0
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/index.js +1 -0
  31. package/dist/utils/index.js.map +1 -1
  32. package/dist/web/public/admin-ui.js.gz +0 -0
  33. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  34. package/dist/web/public/{app.adc992cd.js → app.5805dde4.js} +6 -6
  35. package/dist/web/public/app.5805dde4.js.br +0 -0
  36. package/dist/web/public/app.5805dde4.js.gz +0 -0
  37. package/dist/web/public/constants.e7175b95.js.gz +0 -0
  38. package/dist/web/public/cron-ui.js.gz +0 -0
  39. package/dist/web/public/entrance-animations.js +754 -0
  40. package/dist/web/public/entrance-animations.js.br +0 -0
  41. package/dist/web/public/entrance-animations.js.gz +0 -0
  42. package/dist/web/public/i18n.04d07c98.js +1 -0
  43. package/dist/web/public/i18n.04d07c98.js.br +0 -0
  44. package/dist/web/public/i18n.04d07c98.js.gz +0 -0
  45. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  46. package/dist/web/public/index.html +28 -9
  47. package/dist/web/public/index.html.br +0 -0
  48. package/dist/web/public/index.html.gz +0 -0
  49. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  50. package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
  51. package/dist/web/public/mobile-handlers.bc2b8c1d.js.gz +0 -0
  52. package/dist/web/public/mobile.f8466b0a.css +1 -0
  53. package/dist/web/public/mobile.f8466b0a.css.br +0 -0
  54. package/dist/web/public/mobile.f8466b0a.css.gz +0 -0
  55. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  56. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  57. package/dist/web/public/{panels-ui.3910e1e2.js → panels-ui.600881ae.js} +17 -17
  58. package/dist/web/public/panels-ui.600881ae.js.br +0 -0
  59. package/dist/web/public/panels-ui.600881ae.js.gz +0 -0
  60. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  61. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  62. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  63. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  64. package/dist/web/public/{session-ui.2d3541c6.js → session-ui.aab84498.js} +3 -3
  65. package/dist/web/public/session-ui.aab84498.js.br +0 -0
  66. package/dist/web/public/session-ui.aab84498.js.gz +0 -0
  67. package/dist/web/public/{settings-ui.08c08e71.js → settings-ui.1e126e75.js} +1 -1
  68. package/dist/web/public/settings-ui.1e126e75.js.br +0 -0
  69. package/dist/web/public/settings-ui.1e126e75.js.gz +0 -0
  70. package/dist/web/public/styles.7e638f69.css +1 -0
  71. package/dist/web/public/styles.7e638f69.css.br +0 -0
  72. package/dist/web/public/styles.7e638f69.css.gz +0 -0
  73. package/dist/web/public/{subagent-windows.69b60c35.js → subagent-windows.4cc8b005.js} +27 -3
  74. package/dist/web/public/subagent-windows.4cc8b005.js.br +0 -0
  75. package/dist/web/public/subagent-windows.4cc8b005.js.gz +0 -0
  76. package/dist/web/public/sw.js.gz +0 -0
  77. package/dist/web/public/terminal-ui.f3a51974.js.gz +0 -0
  78. package/dist/web/public/ultracode-panel.js.gz +0 -0
  79. package/dist/web/public/ultracode-windows.js +4 -0
  80. package/dist/web/public/ultracode-windows.js.br +0 -0
  81. package/dist/web/public/ultracode-windows.js.gz +0 -0
  82. package/dist/web/public/upload.html.gz +0 -0
  83. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  84. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  85. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  86. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  87. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  88. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  89. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  90. package/dist/web/public/vendor/xterm.css.gz +0 -0
  91. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  92. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  93. package/dist/web/public/webview-tabs.js.gz +0 -0
  94. package/dist/web/routes/session-routes.d.ts.map +1 -1
  95. package/dist/web/routes/session-routes.js +59 -25
  96. package/dist/web/routes/session-routes.js.map +1 -1
  97. package/dist/web/routes/system-routes.d.ts.map +1 -1
  98. package/dist/web/routes/system-routes.js +9 -1
  99. package/dist/web/routes/system-routes.js.map +1 -1
  100. package/dist/web/schemas.d.ts +16 -0
  101. package/dist/web/schemas.d.ts.map +1 -1
  102. package/dist/web/schemas.js +24 -5
  103. package/dist/web/schemas.js.map +1 -1
  104. package/dist/web/server.d.ts.map +1 -1
  105. package/dist/web/server.js +1 -0
  106. package/dist/web/server.js.map +1 -1
  107. package/package.json +1 -1
  108. package/dist/web/public/app.adc992cd.js.br +0 -0
  109. package/dist/web/public/app.adc992cd.js.gz +0 -0
  110. package/dist/web/public/i18n.4dcf1b87.js +0 -1
  111. package/dist/web/public/i18n.4dcf1b87.js.br +0 -0
  112. package/dist/web/public/i18n.4dcf1b87.js.gz +0 -0
  113. package/dist/web/public/mobile.40e2d64e.css +0 -1
  114. package/dist/web/public/mobile.40e2d64e.css.br +0 -0
  115. package/dist/web/public/mobile.40e2d64e.css.gz +0 -0
  116. package/dist/web/public/panels-ui.3910e1e2.js.br +0 -0
  117. package/dist/web/public/panels-ui.3910e1e2.js.gz +0 -0
  118. package/dist/web/public/session-ui.2d3541c6.js.br +0 -0
  119. package/dist/web/public/session-ui.2d3541c6.js.gz +0 -0
  120. package/dist/web/public/settings-ui.08c08e71.js.br +0 -0
  121. package/dist/web/public/settings-ui.08c08e71.js.gz +0 -0
  122. package/dist/web/public/styles.30e64547.css +0 -1
  123. package/dist/web/public/styles.30e64547.css.br +0 -0
  124. package/dist/web/public/styles.30e64547.css.gz +0 -0
  125. package/dist/web/public/subagent-windows.69b60c35.js.br +0 -0
  126. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
@@ -0,0 +1,754 @@
1
+ /**
2
+ * @fileoverview Entrance animations for the four things that appear when work
3
+ * starts: session TABS, the main TERMINAL pane a session's CLI runs in, floating
4
+ * agent WINDOWS, and the CONNECTION LINES tying a window back to its parent tab.
5
+ * One picker per surface, plus themes that set all four to a matching look.
6
+ *
7
+ * Everything is OFF by default (the `legacy` theme), so an untouched install
8
+ * behaves exactly as it did before this module existed. Opt in via App Settings
9
+ * → Appearance → Entrance Animations.
10
+ *
11
+ * Four constraints shape the design:
12
+ *
13
+ * 1. `_fullRenderSessionTabs()` replaces the tab strip's entire innerHTML, and
14
+ * `_updateConnectionLinesImmediate()` does `svg.innerHTML = ''` and rebuilds
15
+ * every path. Both run constantly while sessions and agents are spawning, so
16
+ * an animating tab or line element is DESTROYED mid-flight. Those two are
17
+ * therefore tracked by id in `_tabEnterActive` / `_lineEnterActive` and
18
+ * re-applied to the fresh element with a NEGATIVE animation-delay, resuming at
19
+ * the same offset instead of restarting or snapping to the end. Windows are
20
+ * stable DOM and need none of this.
21
+ * 2. Connection-line geometry comes from `getBoundingClientRect()` on the window.
22
+ * A window entrance that starts with a transform would move that rect, so the
23
+ * `beam` style (which must hold still while its line draws toward it) animates
24
+ * opacity and filter only. Every other style refreshes the lines when it ends.
25
+ * 3. The terminal pane is ONE shared element, so its entrance is marked at
26
+ * session creation but played at selection: a session created in the
27
+ * background must not animate the pane the user is currently looking at. Its
28
+ * styles are also restricted to transform/opacity/clip-path (see below).
29
+ * 4. Nothing may animate on page load or reconnect replay. Only ids that pass
30
+ * through `markSessionTabEntering()` animate, and `_tabEnterSeen` makes that
31
+ * once-per-id even though the POST response and the SSE event both call
32
+ * `_onSessionCreated`.
33
+ *
34
+ * Styles are selected by `data-tab-anim` / `data-term-anim` / `data-win-anim` /
35
+ * `data-line-anim` on <html>; the keyframes live in styles.css. `?animlab=1`
36
+ * opens a floating picker that fakes tabs, a pane replay, a window and a line,
37
+ * so styles can be compared without spawning real sessions or agents.
38
+ *
39
+ * @mixin Extends CodemanApp.prototype via Object.assign
40
+ * @dependency app.js (tab render pipeline), subagent-windows.js (window + line hooks)
41
+ * @dependency constants.js (escapeHtml)
42
+ * @loadorder 12.6 of 16, after webview-tabs.js, before ralph-wizard.js
43
+ */
44
+
45
+ /** Tab entrance styles. `key` doubles as the `data-tab-anim` value. */
46
+ const TAB_ANIM_STYLES = [
47
+ { key: 'slide', label: 'Slide', blurb: 'Drifts in from the right.', duration: 380 },
48
+ { key: 'pop', label: 'Pop', blurb: 'Springs past full size, then settles.', duration: 460 },
49
+ { key: 'crt', label: 'CRT', blurb: 'Snaps open as a hot line, then unfolds.', duration: 520 },
50
+ { key: 'unroll', label: 'Unroll', blurb: 'The strip makes room and the tab widens in.', duration: 480 },
51
+ { key: 'boot', label: 'Boot', blurb: 'Flickers on under a green scan sweep.', duration: 720 },
52
+ { key: 'flip', label: 'Flip', blurb: 'Drops in as a card hinged on its top edge.', duration: 520 },
53
+ { key: 'off', label: 'Off', blurb: 'Tabs just appear.', duration: 0 },
54
+ ];
55
+
56
+ /**
57
+ * Window entrance styles. `fly` is the pre-existing behaviour (the window flies
58
+ * out of its parent tab via a JS transition in subagent-windows.js); every other
59
+ * style positions the window at its resting spot and runs a CSS animation there.
60
+ */
61
+ const WIN_ANIM_STYLES = [
62
+ { key: 'fly', label: 'Fly from tab', blurb: 'Current behaviour: flies out of the tab, scaling up.', duration: 400 },
63
+ { key: 'crt', label: 'CRT', blurb: 'Bursts open as a hot line, then unfolds vertically.', duration: 560 },
64
+ { key: 'materialize', label: 'Materialize', blurb: 'Resolves out of a blur with a short glitch.', duration: 620 },
65
+ { key: 'unfold', label: 'Unfold', blurb: 'Hinges down from its top edge in 3D.', duration: 560 },
66
+ { key: 'beam', label: 'Beam down', blurb: 'Waits for its line to reach it, then materializes.', duration: 620 },
67
+ { key: 'pop', label: 'Pop', blurb: 'Springs open from its centre.', duration: 460 },
68
+ { key: 'off', label: 'Off', blurb: 'Windows just appear.', duration: 0 },
69
+ ];
70
+
71
+ /** Connection-line entrance styles. `key` doubles as the `data-line-anim` value. */
72
+ const LINE_ANIM_STYLES = [
73
+ { key: 'draw', label: 'Draw', blurb: 'Draws itself from the tab down to the window.', duration: 420 },
74
+ { key: 'packet', label: 'Packet', blurb: 'Line fades in, then a bright packet runs down it.', duration: 700 },
75
+ { key: 'fade', label: 'Fade', blurb: 'Simply fades in.', duration: 300 },
76
+ { key: 'off', label: 'Off', blurb: 'Lines just appear.', duration: 0 },
77
+ ];
78
+
79
+ /**
80
+ * Main-terminal entrance styles: the pane a session's CLI actually runs in.
81
+ *
82
+ * ⚠ These may only animate transform, opacity and clip-path. xterm's FitAddon
83
+ * derives rows/cols from `getComputedStyle(parent).width/height`, which reports
84
+ * the untransformed layout box, so transforms are safe, but animating width,
85
+ * height or padding would feed wrong dimensions into `resize()` and through to
86
+ * the PTY. Colour washes go on `.terminal-container::before`, never a `filter`
87
+ * on the container: that would blur a full-screen WebGL canvas every frame.
88
+ */
89
+ const TERM_ANIM_STYLES = [
90
+ { key: 'crt', label: 'CRT', blurb: 'Power-on: a hot line that expands to full height.', duration: 560 },
91
+ { key: 'boot', label: 'Boot', blurb: 'Flickers on under a green scan sweep.', duration: 760 },
92
+ { key: 'wipe', label: 'Wipe', blurb: 'Reveals top-to-bottom behind a bright edge.', duration: 520 },
93
+ { key: 'slide', label: 'Slide up', blurb: 'Rises into place from below.', duration: 420 },
94
+ { key: 'fade', label: 'Fade', blurb: 'Quiet fade with a touch of scale.', duration: 340 },
95
+ { key: 'off', label: 'Off', blurb: 'Current behaviour: the pane just appears.', duration: 0 },
96
+ ];
97
+
98
+ /** How long a `beam` window waits before materializing. Just under the line draw. */
99
+ const BEAM_HOLD_MS = 360;
100
+
101
+ /** One-click combinations that read as a single look. */
102
+ const ANIM_THEMES = [
103
+ { key: 'terminal', label: 'Terminal', tab: 'crt', win: 'crt', line: 'draw', term: 'crt' },
104
+ { key: 'beamdown', label: 'Beam down', tab: 'crt', win: 'beam', line: 'draw', term: 'wipe' },
105
+ { key: 'quiet', label: 'Quiet', tab: 'slide', win: 'materialize', line: 'fade', term: 'fade' },
106
+ { key: 'playful', label: 'Playful', tab: 'pop', win: 'pop', line: 'packet', term: 'slide' },
107
+ { key: 'legacy', label: 'Legacy', tab: 'off', win: 'fly', line: 'off', term: 'off' },
108
+ ];
109
+
110
+ /**
111
+ * Defaults are the `legacy` theme: every entrance OFF, and agent windows on the
112
+ * `fly` behaviour Codeman already had before this module existed. So a user who
113
+ * never opens the picker sees exactly the pre-existing UI, and each mark/apply
114
+ * hook short-circuits on its first line. Opt in via App Settings → Appearance →
115
+ * Entrance Animations, which persists to the localStorage keys below.
116
+ */
117
+ const TAB_ANIM_DEFAULT = 'off';
118
+ const WIN_ANIM_DEFAULT = 'fly';
119
+ const LINE_ANIM_DEFAULT = 'off';
120
+ const TERM_ANIM_DEFAULT = 'off';
121
+ const TAB_ANIM_STAGGER_DEFAULT = 90;
122
+ /** A new id joins the current cascade if it arrives within this of the last one. */
123
+ const TAB_ANIM_BATCH_WINDOW_MS = 600;
124
+
125
+ const ANIM_KEYS = {
126
+ tab: 'codeman:tabAnim',
127
+ win: 'codeman:winAnim',
128
+ line: 'codeman:lineAnim',
129
+ term: 'codeman:termAnim',
130
+ termSwitch: 'codeman:termAnimOnSwitch',
131
+ stagger: 'codeman:tabAnimStagger',
132
+ speed: 'codeman:tabAnimSpeed',
133
+ };
134
+
135
+ Object.assign(CodemanApp.prototype, {
136
+ // ── Setup ─────────────────────────────────────────────────────────────────
137
+
138
+ /** Resolve every style/timing and stamp them on <html>. Called once at startup. */
139
+ initEntranceAnimations() {
140
+ this._tabEnterSeen = new Set();
141
+ this._tabEnterActive = new Map();
142
+ this._lineEnterActive = new Map();
143
+ this._tabEnterBatchIndex = 0;
144
+ this._tabEnterLastMarkTs = 0;
145
+
146
+ const params = new URLSearchParams(location.search);
147
+ // ?tabanim= / ?winanim= / ?lineanim= win for a single load, so a style can be
148
+ // tried without persisting over whatever is saved.
149
+ const pick = (param, styles, storeKey, fallback) => {
150
+ const fromUrl = params.get(param);
151
+ if (styles.some((s) => s.key === fromUrl)) return fromUrl;
152
+ return this._animRead(storeKey, fallback);
153
+ };
154
+
155
+ this.setTabAnimStyle(pick('tabanim', TAB_ANIM_STYLES, ANIM_KEYS.tab, TAB_ANIM_DEFAULT), { persist: false });
156
+ this.setWinAnimStyle(pick('winanim', WIN_ANIM_STYLES, ANIM_KEYS.win, WIN_ANIM_DEFAULT), { persist: false });
157
+ this.setLineAnimStyle(pick('lineanim', LINE_ANIM_STYLES, ANIM_KEYS.line, LINE_ANIM_DEFAULT), { persist: false });
158
+ this.setTermAnimStyle(pick('termanim', TERM_ANIM_STYLES, ANIM_KEYS.term, TERM_ANIM_DEFAULT), { persist: false });
159
+ this.setTermAnimOnSwitch(this._animRead(ANIM_KEYS.termSwitch, '0') === '1', { persist: false });
160
+
161
+ this.setTabAnimStagger(Number(this._animRead(ANIM_KEYS.stagger, TAB_ANIM_STAGGER_DEFAULT)), { persist: false });
162
+ this.setAnimSpeed(Number(this._animRead(ANIM_KEYS.speed, 1)), { persist: false });
163
+
164
+ if (params.get('animlab') === '1' || params.get('tabanimlab') === '1') this.openAnimLab();
165
+ },
166
+
167
+ _animRead(key, fallback) {
168
+ try {
169
+ const raw = localStorage.getItem(key);
170
+ return raw === null || raw === '' ? fallback : raw;
171
+ } catch {
172
+ return fallback;
173
+ }
174
+ },
175
+
176
+ _animWrite(key, value) {
177
+ try {
178
+ localStorage.setItem(key, String(value));
179
+ } catch {
180
+ /* private mode / quota, the in-memory value still applies for this load */
181
+ }
182
+ },
183
+
184
+ _setAnimStyle(prop, key, styles, fallback, attr, storeKey, persist) {
185
+ const style = styles.some((s) => s.key === key) ? key : fallback;
186
+ this[prop] = style;
187
+ document.documentElement.setAttribute(attr, style);
188
+ if (persist) this._animWrite(storeKey, style);
189
+ // Keep the lab's highlight honest when a style is set from anywhere other
190
+ // than the lab's own buttons (a theme, a URL param, the console).
191
+ this._syncAnimLab?.();
192
+ },
193
+
194
+ setTabAnimStyle(key, { persist = true } = {}) {
195
+ // prettier-ignore
196
+ this._setAnimStyle('_tabAnimStyle', key, TAB_ANIM_STYLES, TAB_ANIM_DEFAULT, 'data-tab-anim', ANIM_KEYS.tab, persist);
197
+ },
198
+
199
+ setWinAnimStyle(key, { persist = true } = {}) {
200
+ // prettier-ignore
201
+ this._setAnimStyle('_winAnimStyle', key, WIN_ANIM_STYLES, WIN_ANIM_DEFAULT, 'data-win-anim', ANIM_KEYS.win, persist);
202
+ },
203
+
204
+ setLineAnimStyle(key, { persist = true } = {}) {
205
+ // prettier-ignore
206
+ this._setAnimStyle('_lineAnimStyle', key, LINE_ANIM_STYLES, LINE_ANIM_DEFAULT, 'data-line-anim', ANIM_KEYS.line, persist);
207
+ },
208
+
209
+ setTermAnimStyle(key, { persist = true } = {}) {
210
+ // prettier-ignore
211
+ this._setAnimStyle('_termAnimStyle', key, TERM_ANIM_STYLES, TERM_ANIM_DEFAULT, 'data-term-anim', ANIM_KEYS.term, persist);
212
+ },
213
+
214
+ /** Replay the terminal entrance on every tab switch, not just on a new session. */
215
+ setTermAnimOnSwitch(on, { persist = true } = {}) {
216
+ this._termAnimOnSwitch = !!on;
217
+ if (persist) this._animWrite(ANIM_KEYS.termSwitch, on ? '1' : '0');
218
+ this._syncAnimLab?.();
219
+ },
220
+
221
+ /** Apply a theme: one look across tabs, windows, lines and the terminal. */
222
+ setAnimTheme(themeKey) {
223
+ const theme = ANIM_THEMES.find((t) => t.key === themeKey);
224
+ if (!theme) return;
225
+ this.setTabAnimStyle(theme.tab);
226
+ this.setWinAnimStyle(theme.win);
227
+ this.setLineAnimStyle(theme.line);
228
+ this.setTermAnimStyle(theme.term);
229
+ this._syncEntranceAnimSetting?.();
230
+ },
231
+
232
+ /** The theme matching the four current styles, or 'custom' for a lab mix. */
233
+ currentAnimTheme() {
234
+ const match = ANIM_THEMES.find(
235
+ (t) =>
236
+ t.tab === this._tabAnimStyle &&
237
+ t.win === this._winAnimStyle &&
238
+ t.line === this._lineAnimStyle &&
239
+ t.term === this._termAnimStyle
240
+ );
241
+ return match ? match.key : 'custom';
242
+ },
243
+
244
+ // ── App Settings picker ───────────────────────────────────────────────────
245
+ //
246
+ // Wired straight to setAnimTheme() rather than through saveAppSettings(): the
247
+ // styles live in their own localStorage keys, so they stay per-device and never
248
+ // reach `PUT /api/settings`, whose schema is .strict() and would reject them.
249
+
250
+ _syncEntranceAnimSetting() {
251
+ const sel = document.getElementById('appSettingsEntranceAnim');
252
+ if (!sel) return;
253
+ sel.value = this.currentAnimTheme();
254
+ if (!sel.dataset.bound) {
255
+ sel.dataset.bound = '1';
256
+ sel.addEventListener('change', () => {
257
+ // 'custom' is a readout of a lab mix, not something you can select into.
258
+ if (sel.value === 'custom') sel.value = this.currentAnimTheme();
259
+ else this.setAnimTheme(sel.value);
260
+ });
261
+ }
262
+ },
263
+
264
+ setTabAnimStagger(ms, { persist = true } = {}) {
265
+ const value = Number.isFinite(ms) ? Math.max(0, Math.min(400, Math.round(ms))) : TAB_ANIM_STAGGER_DEFAULT;
266
+ this._tabAnimStagger = value;
267
+ if (persist) this._animWrite(ANIM_KEYS.stagger, value);
268
+ },
269
+
270
+ setAnimSpeed(multiplier, { persist = true } = {}) {
271
+ const value = Number.isFinite(multiplier) ? Math.max(0.25, Math.min(3, multiplier)) : 1;
272
+ this._animSpeed = value;
273
+ // Every keyframe block reads this, so one variable retimes all of them.
274
+ document.documentElement.style.setProperty('--anim-enter-scale', String(1 / value));
275
+ if (persist) this._animWrite(ANIM_KEYS.speed, value);
276
+ },
277
+
278
+ _styleDuration(styles, key) {
279
+ return (styles.find((s) => s.key === key)?.duration || 0) / (this._animSpeed || 1);
280
+ },
281
+
282
+ _tabAnimDuration() {
283
+ return this._styleDuration(TAB_ANIM_STYLES, this._tabAnimStyle);
284
+ },
285
+
286
+ _winAnimDuration() {
287
+ return this._styleDuration(WIN_ANIM_STYLES, this._winAnimStyle);
288
+ },
289
+
290
+ _lineAnimDuration() {
291
+ return this._styleDuration(LINE_ANIM_STYLES, this._lineAnimStyle);
292
+ },
293
+
294
+ _termAnimDuration() {
295
+ return this._styleDuration(TERM_ANIM_STYLES, this._termAnimStyle);
296
+ },
297
+
298
+ // ── Tabs ──────────────────────────────────────────────────────────────────
299
+
300
+ /** Queue a session id to animate on its next render. Idempotent per id. */
301
+ markSessionTabEntering(id) {
302
+ if (!id || this._tabAnimStyle === 'off') return;
303
+ if (!this._tabEnterSeen) return; // initEntranceAnimations() has not run yet
304
+ if (this._tabEnterSeen.has(id)) return;
305
+ this._tabEnterSeen.add(id);
306
+
307
+ const now = performance.now();
308
+ // A launch landing well after the previous one starts its own cascade rather
309
+ // than inheriting a large stale offset.
310
+ if (now - this._tabEnterLastMarkTs > TAB_ANIM_BATCH_WINDOW_MS) this._tabEnterBatchIndex = 0;
311
+ this._tabEnterLastMarkTs = now;
312
+
313
+ this._tabEnterActive.set(id, {
314
+ startTs: now,
315
+ staggerMs: this._tabEnterBatchIndex * this._tabAnimStagger,
316
+ });
317
+ this._tabEnterBatchIndex += 1;
318
+ },
319
+
320
+ /** Attach (or resume) the entrance animation on freshly rendered tabs. */
321
+ _applyTabEntrances() {
322
+ const active = this._tabEnterActive;
323
+ if (!active || active.size === 0) return;
324
+ if (this._tabAnimStyle === 'off') {
325
+ active.clear();
326
+ return;
327
+ }
328
+
329
+ const container = this.$('sessionTabs');
330
+ if (!container) return;
331
+
332
+ const now = performance.now();
333
+ const duration = this._tabAnimDuration();
334
+
335
+ for (const [id, state] of active) {
336
+ const elapsed = now - state.startTs;
337
+ // Ran to completion while the element was detached, nothing left to show.
338
+ if (elapsed > state.staggerMs + duration + 50) {
339
+ active.delete(id);
340
+ continue;
341
+ }
342
+
343
+ const tab = container.querySelector(`.session-tab[data-id="${CSS.escape(id)}"]`);
344
+ if (!tab) continue; // not rendered yet; a later render picks it up
345
+ // Already running on this element. Re-stamping the delay would jump it, and
346
+ // the incremental render path can reach here for the same element.
347
+ if (tab.classList.contains('tab-enter')) continue;
348
+
349
+ // Negative delay resumes mid-animation, so an unrelated re-render mid-cascade
350
+ // does not restart the tab or make it snap.
351
+ tab.style.setProperty('--tab-enter-delay', `${state.staggerMs - elapsed}ms`);
352
+ tab.classList.add('tab-enter');
353
+ const done = () => {
354
+ tab.classList.remove('tab-enter');
355
+ tab.style.removeProperty('--tab-enter-delay');
356
+ this._tabEnterActive?.delete(id);
357
+ };
358
+ tab.addEventListener('animationend', done, { once: true });
359
+ tab.addEventListener('animationcancel', done, { once: true });
360
+ }
361
+ },
362
+
363
+ // ── Main terminal ─────────────────────────────────────────────────────────
364
+
365
+ /**
366
+ * Queue the terminal pane to animate the first time this session is shown.
367
+ * Marked at creation but PLAYED at selection, because the pane is one shared
368
+ * element: a session created in the background must not animate the pane the
369
+ * user is currently looking at.
370
+ */
371
+ markTerminalEntering(id) {
372
+ if (!id || this._termAnimStyle === 'off') return;
373
+ if (!this._termEnterPending) this._termEnterPending = new Set();
374
+ this._termEnterPending.add(id);
375
+ },
376
+
377
+ /** Play the terminal entrance for `sessionId`, if it is owed one. */
378
+ playTerminalEntrance(sessionId) {
379
+ const style = this._termAnimStyle || TERM_ANIM_DEFAULT;
380
+ if (style === 'off') return;
381
+
382
+ const owed = sessionId && this._termEnterPending?.delete(sessionId);
383
+ if (!owed && !this._termAnimOnSwitch) return;
384
+
385
+ const el = this.$('terminalContainer');
386
+ if (!el) return;
387
+
388
+ // Restart cleanly when switching tabs faster than the animation runs.
389
+ el.classList.remove('term-enter');
390
+ void el.offsetWidth;
391
+ el.classList.add('term-enter');
392
+
393
+ clearTimeout(this._termEnterTimer);
394
+ const done = () => {
395
+ el.classList.remove('term-enter');
396
+ clearTimeout(this._termEnterTimer);
397
+ };
398
+ el.addEventListener('animationend', done, { once: true });
399
+ el.addEventListener('animationcancel', done, { once: true });
400
+ // Backstop: a backgrounded tab never fires animationend, which would leave
401
+ // the pane stuck at its 0% keyframe (invisible) when you come back to it.
402
+ this._termEnterTimer = setTimeout(done, this._termAnimDuration() + 900);
403
+ },
404
+
405
+ // ── Windows ───────────────────────────────────────────────────────────────
406
+
407
+ /**
408
+ * True when the window should be parked on its parent tab and flown to its
409
+ * resting spot by subagent-windows.js. False for every CSS-animated style,
410
+ * which needs the window to start at its final position.
411
+ */
412
+ windowEntranceFliesFromTab() {
413
+ return (this._winAnimStyle || WIN_ANIM_DEFAULT) === 'fly';
414
+ },
415
+
416
+ /**
417
+ * Run the window entrance on an already-positioned window.
418
+ * @param {HTMLElement} win a .subagent-window or .ultracode-window
419
+ */
420
+ applyWindowEntrance(win) {
421
+ if (!win) return;
422
+ const style = this._winAnimStyle || WIN_ANIM_DEFAULT;
423
+ if (style === 'off' || style === 'fly') return;
424
+
425
+ // `beam` holds the window still (opacity/filter only) while its line draws
426
+ // toward it, then materializes. Everything else starts immediately.
427
+ if (style === 'beam') win.style.setProperty('--win-enter-delay', `${BEAM_HOLD_MS / (this._animSpeed || 1)}ms`);
428
+
429
+ win.classList.add('win-enter');
430
+ const done = () => {
431
+ win.classList.remove('win-enter');
432
+ win.style.removeProperty('--win-enter-delay');
433
+ // A transformed window reports a transformed rect, so lines drawn while it
434
+ // was animating are slightly off. Redraw once it has settled.
435
+ this.updateConnectionLines?.();
436
+ };
437
+ win.addEventListener('animationend', done, { once: true });
438
+ win.addEventListener('animationcancel', done, { once: true });
439
+ },
440
+
441
+ // ── Connection lines ──────────────────────────────────────────────────────
442
+
443
+ /** Queue an agent's connection line to draw itself on the next line rebuild. */
444
+ markConnectionLineEntering(agentId) {
445
+ if (!agentId || this._lineAnimStyle === 'off') return;
446
+ if (!this._lineEnterActive) return;
447
+ if (this._lineEnterActive.has(agentId)) return;
448
+ this._lineEnterActive.set(agentId, { startTs: performance.now() });
449
+ },
450
+
451
+ /**
452
+ * Attach (or resume) the draw-in animation on freshly rebuilt paths. Called at
453
+ * the end of `_updateConnectionLinesImmediate()`, which has just thrown away
454
+ * and recreated every path element.
455
+ */
456
+ _applyLineEntrances(svg) {
457
+ const active = this._lineEnterActive;
458
+ if (!active || active.size === 0 || !svg) return;
459
+ const style = this._lineAnimStyle || LINE_ANIM_DEFAULT;
460
+ if (style === 'off') {
461
+ active.clear();
462
+ return;
463
+ }
464
+
465
+ const now = performance.now();
466
+ const duration = this._lineAnimDuration();
467
+
468
+ for (const [agentId, state] of active) {
469
+ const elapsed = now - state.startTs;
470
+ if (elapsed > duration + 50) {
471
+ active.delete(agentId);
472
+ continue;
473
+ }
474
+
475
+ const path = svg.querySelector(`path[data-agent-id="${CSS.escape(agentId)}"]`);
476
+ if (!path) continue;
477
+
478
+ const len = Math.max(1, Math.round(path.getTotalLength()));
479
+ path.style.setProperty('--line-len', `${len}px`);
480
+ path.style.setProperty('--line-enter-delay', `${-elapsed}ms`);
481
+ path.classList.add('line-enter');
482
+
483
+ // `packet` rides a bright dash ON TOP of the normal dashed line, so the base
484
+ // line keeps its look instead of being taken over by the animation.
485
+ if (style === 'packet') {
486
+ const packet = path.cloneNode(false);
487
+ packet.removeAttribute('data-agent-id');
488
+ packet.setAttribute('class', 'connection-line-packet');
489
+ packet.style.setProperty('--line-len', `${len}px`);
490
+ packet.style.setProperty('--line-enter-delay', `${-elapsed}ms`);
491
+ svg.appendChild(packet);
492
+ }
493
+ }
494
+ },
495
+
496
+ // ── Lab (compare styles without spawning sessions or agents) ───────────────
497
+
498
+ /** Floating picker: switch styles per surface and replay fake entrances. */
499
+ openAnimLab() {
500
+ if (document.getElementById('animLab')) return;
501
+
502
+ const group = (title, styles, attr) => `
503
+ <div class="anim-lab-group">
504
+ <div class="anim-lab-group-title">${title}</div>
505
+ ${styles
506
+ .map(
507
+ (s) => `<button type="button" class="anim-lab-style" data-attr="${attr}" data-style="${s.key}">
508
+ <strong>${escapeHtml(s.label)}</strong><em>${escapeHtml(s.blurb)}</em>
509
+ </button>`
510
+ )
511
+ .join('')}
512
+ </div>`;
513
+
514
+ const panel = document.createElement('div');
515
+ panel.id = 'animLab';
516
+ panel.className = 'anim-lab';
517
+ panel.innerHTML = `
518
+ <div class="anim-lab-head">
519
+ <span>Entrance lab</span>
520
+ <button type="button" class="anim-lab-close" aria-label="Close">&times;</button>
521
+ </div>
522
+ <div class="anim-lab-themes">
523
+ ${ANIM_THEMES.map((t) => `<button type="button" data-theme="${t.key}">${escapeHtml(t.label)}</button>`).join('')}
524
+ </div>
525
+ <div class="anim-lab-scroll">
526
+ ${group('Tabs', TAB_ANIM_STYLES, 'tab')}
527
+ ${group('Terminal pane', TERM_ANIM_STYLES, 'term')}
528
+ <label class="anim-lab-check">
529
+ <input type="checkbox" data-check="termSwitch"> Also on every tab switch
530
+ </label>
531
+ ${group('Agent windows', WIN_ANIM_STYLES, 'win')}
532
+ ${group('Connection lines', LINE_ANIM_STYLES, 'line')}
533
+ </div>
534
+ <label class="anim-lab-range">Tab stagger <output data-out="stagger"></output>
535
+ <input type="range" data-range="stagger" min="0" max="260" step="10">
536
+ </label>
537
+ <label class="anim-lab-range">Speed <output data-out="speed"></output>
538
+ <input type="range" data-range="speed" min="0.5" max="2" step="0.1">
539
+ </label>
540
+ <div class="anim-lab-demo">
541
+ <span>Replay</span>
542
+ <button type="button" data-demo="tabs">Tabs</button>
543
+ <button type="button" data-demo="term">Pane</button>
544
+ <button type="button" data-demo="window">Window</button>
545
+ <button type="button" data-demo="all">All</button>
546
+ </div>
547
+ <p class="anim-lab-hint">Fake tabs, window and line, removed after the run. Real launches use the same timing.</p>
548
+ `;
549
+ document.body.appendChild(panel);
550
+
551
+ panel.querySelector('.anim-lab-close').addEventListener('click', () => this.closeAnimLab());
552
+ panel.querySelectorAll('button[data-theme]').forEach((btn) => {
553
+ btn.addEventListener('click', () => {
554
+ this.setAnimTheme(btn.dataset.theme);
555
+ this._syncAnimLab();
556
+ this.demoEntrance('all');
557
+ });
558
+ });
559
+ panel.querySelectorAll('.anim-lab-style').forEach((btn) => {
560
+ btn.addEventListener('click', () => {
561
+ const { attr, style } = btn.dataset;
562
+ if (attr === 'tab') this.setTabAnimStyle(style);
563
+ else if (attr === 'win') this.setWinAnimStyle(style);
564
+ else if (attr === 'term') this.setTermAnimStyle(style);
565
+ else this.setLineAnimStyle(style);
566
+ this._syncAnimLab();
567
+ this.demoEntrance({ tab: 'tabs', term: 'term' }[attr] || 'all');
568
+ });
569
+ });
570
+ panel.querySelector('input[data-check="termSwitch"]').addEventListener('change', (e) => {
571
+ this.setTermAnimOnSwitch(e.target.checked);
572
+ });
573
+ panel.querySelectorAll('input[data-range]').forEach((input) => {
574
+ input.addEventListener('input', () => {
575
+ if (input.dataset.range === 'stagger') this.setTabAnimStagger(Number(input.value));
576
+ else this.setAnimSpeed(Number(input.value));
577
+ this._syncAnimLab();
578
+ });
579
+ input.addEventListener('change', () => this.demoEntrance('all'));
580
+ });
581
+ panel.querySelectorAll('button[data-demo]').forEach((btn) => {
582
+ btn.addEventListener('click', () => this.demoEntrance(btn.dataset.demo));
583
+ });
584
+
585
+ this._syncAnimLab();
586
+ },
587
+
588
+ closeAnimLab() {
589
+ document.getElementById('animLab')?.remove();
590
+ this._clearEntranceDemo();
591
+ },
592
+
593
+ _syncAnimLab() {
594
+ const panel = document.getElementById('animLab');
595
+ if (!panel) return;
596
+ const current = {
597
+ tab: this._tabAnimStyle,
598
+ win: this._winAnimStyle,
599
+ line: this._lineAnimStyle,
600
+ term: this._termAnimStyle,
601
+ };
602
+ panel.querySelectorAll('.anim-lab-style').forEach((btn) => {
603
+ btn.classList.toggle('selected', current[btn.dataset.attr] === btn.dataset.style);
604
+ });
605
+ panel.querySelectorAll('button[data-theme]').forEach((btn) => {
606
+ const t = ANIM_THEMES.find((x) => x.key === btn.dataset.theme);
607
+ btn.classList.toggle('selected', !!t && ['tab', 'win', 'line', 'term'].every((k) => t[k] === current[k]));
608
+ });
609
+ const check = panel.querySelector('input[data-check="termSwitch"]');
610
+ if (check) check.checked = !!this._termAnimOnSwitch;
611
+ panel.querySelector('input[data-range="stagger"]').value = String(this._tabAnimStagger);
612
+ panel.querySelector('output[data-out="stagger"]').textContent = `${this._tabAnimStagger}ms`;
613
+ panel.querySelector('input[data-range="speed"]').value = String(this._animSpeed);
614
+ panel.querySelector('output[data-out="speed"]').textContent = `${this._animSpeed.toFixed(1)}x`;
615
+ },
616
+
617
+ _clearEntranceDemo() {
618
+ clearTimeout(this._animDemoTimer);
619
+ clearTimeout(this._animDemoWindowTimer);
620
+ document.querySelectorAll('.session-tab[data-demo]').forEach((el) => el.remove());
621
+ document.querySelectorAll('.subagent-window[data-demo]').forEach((el) => el.remove());
622
+ document.getElementById('animLabLines')?.remove();
623
+ },
624
+
625
+ /**
626
+ * The demo line gets its OWN svg overlay rather than sharing #connectionLines.
627
+ * That overlay is rebuilt from real windows via `svg.innerHTML = ''`, so a fake
628
+ * path dropped into it is erased the moment anything triggers a redraw -
629
+ * including the demo window's own entrance finishing.
630
+ */
631
+ _demoLineSvg() {
632
+ let svg = document.getElementById('animLabLines');
633
+ if (!svg) {
634
+ svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
635
+ svg.id = 'animLabLines';
636
+ svg.setAttribute('class', 'connection-lines-svg');
637
+ document.body.appendChild(svg);
638
+ }
639
+ return svg;
640
+ },
641
+
642
+ /** @param {'tabs'|'term'|'window'|'all'} what */
643
+ demoEntrance(what = 'all') {
644
+ this._clearEntranceDemo();
645
+
646
+ // The pane is a real, shared element rather than a throwaway, so replay it
647
+ // through the same entry point a real launch uses (bypassing the owed-id
648
+ // check, which only exists to keep background sessions from hijacking it).
649
+ if (what === 'term' || what === 'all') {
650
+ const wasOnSwitch = this._termAnimOnSwitch;
651
+ this._termAnimOnSwitch = true;
652
+ this.playTerminalEntrance(null);
653
+ this._termAnimOnSwitch = wasOnSwitch;
654
+ }
655
+ if (what === 'term') return;
656
+
657
+ const tabCount = what === 'window' ? 1 : 4;
658
+ const tabs = this._demoTabs(tabCount);
659
+ let total = Math.max(this._tabAnimDuration() + tabCount * this._tabAnimStagger, this._termAnimDuration());
660
+
661
+ if (what !== 'tabs') {
662
+ // Give the tab cascade a beat, so the window reads as coming out of it.
663
+ const lead = what === 'all' ? Math.min(320, this._tabAnimStagger * 2) : 0;
664
+ this._animDemoWindowTimer = setTimeout(() => this._demoWindow(tabs[0]), lead);
665
+ total = Math.max(total, lead + this._winAnimDuration() + this._lineAnimDuration() + BEAM_HOLD_MS);
666
+ }
667
+
668
+ this._animDemoTimer = setTimeout(() => this._clearEntranceDemo(), total + 1800);
669
+ },
670
+
671
+ /** Append `count` throwaway tabs and run the tab entrance on them. */
672
+ _demoTabs(count) {
673
+ const container = this.$('sessionTabs');
674
+ if (!container) return [];
675
+ const made = [];
676
+ const base = this.sessions?.size || 0;
677
+
678
+ for (let i = 0; i < count; i++) {
679
+ const tab = document.createElement('div');
680
+ tab.className = 'session-tab';
681
+ tab.dataset.demo = '1';
682
+ tab.dataset.color = ['green', 'blue', 'purple', 'orange', 'pink', 'yellow', 'red'][i % 7];
683
+ tab.innerHTML = `
684
+ <span class="tab-number">${base + i + 1}</span>
685
+ <span class="tab-status idle" aria-hidden="true"></span>
686
+ <span class="tab-info"><span class="tab-name-row">
687
+ <span class="tab-name">w${base + i + 1}-demo</span>
688
+ </span></span>`;
689
+ container.appendChild(tab);
690
+ made.push(tab);
691
+ if (this._tabAnimStyle !== 'off') {
692
+ tab.style.setProperty('--tab-enter-delay', `${i * this._tabAnimStagger}ms`);
693
+ void tab.offsetWidth; // force layout so the class add starts a fresh run
694
+ tab.classList.add('tab-enter');
695
+ }
696
+ }
697
+ return made;
698
+ },
699
+
700
+ /** Append a throwaway agent window under `originTab`, plus its connection line. */
701
+ _demoWindow(originTab) {
702
+ const tabRect = originTab?.getBoundingClientRect();
703
+ const win = document.createElement('div');
704
+ win.className = 'subagent-window';
705
+ win.dataset.demo = '1';
706
+ win.style.width = '360px';
707
+ win.style.height = '220px';
708
+ win.style.left = `${Math.max(24, (tabRect?.left ?? 120) - 40)}px`;
709
+ win.style.top = `${(tabRect?.bottom ?? 60) + 160}px`;
710
+ win.style.zIndex = '1001';
711
+ win.innerHTML = `
712
+ <div class="subagent-window-header">
713
+ <div class="subagent-window-title"><span class="icon">&#129302;</span><span class="id">demo-agent</span>
714
+ <span class="status running">running</span></div>
715
+ </div>
716
+ <div class="subagent-window-body"><div class="subagent-empty">Preview window</div></div>`;
717
+ document.body.appendChild(win);
718
+ this.applyWindowEntrance(win);
719
+ this._demoLine(tabRect, win);
720
+ },
721
+
722
+ /** Draw a fake tab→window line into the lab's own overlay and animate it. */
723
+ _demoLine(tabRect, win) {
724
+ if (!tabRect) return;
725
+ const svg = this._demoLineSvg();
726
+ const winRect = win.getBoundingClientRect();
727
+ const x1 = tabRect.left + tabRect.width / 2;
728
+ const y1 = tabRect.bottom;
729
+ const x2 = winRect.left + winRect.width / 2;
730
+ const y2 = winRect.top;
731
+ const midY = (y1 + y2) / 2;
732
+
733
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
734
+ path.setAttribute('d', `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`);
735
+ path.setAttribute('class', 'connection-line');
736
+ path.dataset.demo = '1';
737
+ svg.appendChild(path);
738
+
739
+ if ((this._lineAnimStyle || LINE_ANIM_DEFAULT) === 'off') return;
740
+ const len = Math.max(1, Math.round(path.getTotalLength()));
741
+ path.style.setProperty('--line-len', `${len}px`);
742
+ path.style.setProperty('--line-enter-delay', '0ms');
743
+ path.classList.add('line-enter');
744
+
745
+ if (this._lineAnimStyle === 'packet') {
746
+ const packet = path.cloneNode(false);
747
+ packet.setAttribute('class', 'connection-line-packet');
748
+ packet.dataset.demo = '1';
749
+ packet.style.setProperty('--line-len', `${len}px`);
750
+ packet.style.setProperty('--line-enter-delay', '0ms');
751
+ svg.appendChild(packet);
752
+ }
753
+ },
754
+ });