anentrypoint-design 0.0.458 → 0.0.459

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.458",
3
+ "version": "0.0.459",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -89,6 +89,35 @@ export function ToolCallNode(p) {
89
89
  );
90
90
  }
91
91
 
92
+ // Approval-request card for the freddie wire protocol's approval.request event
93
+ // (plugins/gui/gui-agent): a gated tool call pauses mid-turn until the user
94
+ // resolves it here. p.onResolve({approved, always?}) sends the decision back
95
+ // over the same channel; once resolved the card renders the settled state.
96
+ export function ApprovalNode(p) {
97
+ const status = p.status || 'pending';
98
+ const argsText = typeof p.args === 'string' ? p.args : JSON.stringify(p.args || {}, null, 2);
99
+ const iconName = status === 'pending' ? 'warn' : (status === 'approved' ? 'check' : 'warn');
100
+ const decide = (decision) => (e) => { e.preventDefault(); if (p.onResolve) p.onResolve(decision); };
101
+ return h('div', { class: 'chat-bubble chat-tool chat-approval tool-' + (status === 'pending' ? 'running' : status) },
102
+ h('div', { class: 'chat-tool-head' },
103
+ h('span', { class: 'chat-tool-icon', 'aria-hidden': 'true' }, Icon(iconName, { size: 14 })),
104
+ h('span', { class: 'chat-tool-name' }, 'approval: ' + (p.name || 'tool')),
105
+ h('span', { class: 'chat-tool-status' }, status)
106
+ ),
107
+ h('div', { class: 'chat-tool-body' },
108
+ h('div', { class: 'chat-tool-section' },
109
+ h('div', { class: 'chat-tool-section-label' }, h('span', {}, 'args')),
110
+ h('pre', { class: 'chat-tool-pre' }, h('code', {}, argsText))),
111
+ status === 'pending'
112
+ ? h('div', { class: 'chat-approval-actions' },
113
+ h('button', { type: 'button', class: 'chat-code-copy chat-approval-btn', onclick: decide({ approved: true }) }, 'approve'),
114
+ h('button', { type: 'button', class: 'chat-code-copy chat-approval-btn', onclick: decide({ approved: true, always: true }) }, 'always'),
115
+ h('button', { type: 'button', class: 'chat-code-copy chat-approval-btn', onclick: decide({ approved: false }) }, 'reject'))
116
+ : h('div', { class: 'chat-approval-note' }, status === 'approved' ? (p.always ? 'approved (always, this turn)' : 'approved') : 'rejected')
117
+ )
118
+ );
119
+ }
120
+
92
121
  export function ThinkingNode(p) {
93
122
  if (p.settled) {
94
123
  return h('details', { class: 'chat-bubble chat-thinking-settled' },
@@ -7,7 +7,7 @@ import { Icon } from '../shell.js';
7
7
  import { fmtFileSize } from '../files.js';
8
8
  import { safeUrl, renderInline, fileIconName } from './inline.js';
9
9
  import { MdNode, CodeNode } from './prose-nodes.js';
10
- import { ToolCallNode, ThinkingNode } from './agent-nodes.js';
10
+ import { ToolCallNode, ThinkingNode, ApprovalNode } from './agent-nodes.js';
11
11
 
12
12
  const h = webjsx.createElement;
13
13
 
@@ -35,6 +35,7 @@ export const PART_RENDERERS = {
35
35
  tool_call: (p) => ToolCallNode(p),
36
36
  tool_result: (p) => ToolCallNode({ ...p, name: p.name || 'tool_result', result: p.text != null ? p.text : p.result }),
37
37
  thinking: (p) => ThinkingNode(p),
38
+ approval: (p) => ApprovalNode(p),
38
39
  image: (p) => {
39
40
  // Guard both the wrapping link and the img src against unsafe schemes
40
41
  // (e.g. a data:text/html src) so an embedded-image part from untrusted
@@ -1,5 +1,19 @@
1
- // Freddie conversational pages: the streaming `chat` page (SSE over POST,
2
- // offline outbox queueing, abortable turns) and the `voice` backend probe.
1
+ // Freddie conversational pages: the `chat` agent-workspace page (WebSocket
2
+ // over the freddie wire protocol replay + live turn events + prompt/steer/
3
+ // cancel/approve, no more 120s POST ceiling) and the `voice` backend probe.
4
+ //
5
+ // Transport: WS /api/agent/stream?sessionId=<id> (plugins/gui/gui-agent).
6
+ // The page owns the session id (generated client-side so the socket can
7
+ // subscribe BEFORE the first prompt) and rebuilds its transcript from the
8
+ // server's wire log on (re)connect, so a refresh mid-turn loses nothing.
9
+ // The legacy POST /api/chat path remains only for the offline outbox flush.
10
+ //
11
+ // Event -> transcript mapping (freddie wire envelope {v,event,sessionId,ts,data}):
12
+ // message.append(user|assistant) -> thread messages
13
+ // tool.start / tool.end -> interleaved tool cards (running -> done/error)
14
+ // approval.request / .resolved -> ApprovalNode cards (approve/always/reject)
15
+ // steer.append -> user message (mid-turn injection)
16
+ // session.error -> error pinned to the live assistant turn
3
17
 
4
18
  import * as webjsx from '../../../vendor/webjsx/index.js';
5
19
  import { makePage, api, loadingState, emptyState } from './runtime.js';
@@ -9,20 +23,73 @@ import { formatTime } from '../../locale.js';
9
23
  import { queueMessage, watchReconnect, isOnline } from '../../idb-outbox.js';
10
24
  import { AgentChat } from '../agent-chat.js';
11
25
  import { section, noteAlert } from './shared.js';
12
- import { parseSseStream, toolProgressPart, partsFromMessages } from './sse.js';
13
26
 
14
27
  const h = webjsx.createElement;
15
28
 
29
+ function newSessionId() {
30
+ return (crypto.randomUUID ? crypto.randomUUID() : 's' + Date.now().toString(36) + Math.random().toString(16).slice(2));
31
+ }
32
+
33
+ // Apply one wire envelope to a messages array (shared by replay rebuild and
34
+ // the live stream). `sendApprove` is only needed for live approval cards.
35
+ function applyEnvelope(msgs, env, sendApprove) {
36
+ const { event, data } = env;
37
+ const ts = new Date(env.ts).getTime();
38
+ const lastAssistant = () => { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i].role === 'assistant') return msgs[i]; return null; };
39
+ // The page echoes its own sends into the thread optimistically; the server
40
+ // then emits the SAME user/steer text as an authoritative event. The local
41
+ // echo is followed by the placeholder assistant bubble, so scan back past
42
+ // it (and any parts) for a matching user message before appending a dupe.
43
+ const isDupUser = (text) => {
44
+ for (let i = msgs.length - 1; i >= 0 && i >= msgs.length - 3; i--) {
45
+ if (msgs[i].role === 'user' && msgs[i].content === (text || '')) return true;
46
+ }
47
+ return false;
48
+ };
49
+ if (event === 'message.append') {
50
+ if (data.role === 'user') { if (!isDupUser(data.content)) msgs.push({ id: 'u' + msgs.length + env.ts, role: 'user', content: data.content || '', time: formatTime(ts) }); }
51
+ else if (data.role === 'assistant') {
52
+ const last = msgs[msgs.length - 1];
53
+ // A new assistant turn starts after a user message; consecutive
54
+ // assistant appends within one turn update the SAME bubble.
55
+ if (last && last.role === 'assistant' && last._live) { if (data.content) last.content = data.content; }
56
+ else msgs.push({ id: 'a' + msgs.length + env.ts, role: 'assistant', content: data.content || '', parts: [], time: formatTime(ts), _live: true });
57
+ }
58
+ } else if (event === 'steer.append') {
59
+ if (!isDupUser(data.text)) msgs.push({ id: 'u' + msgs.length + env.ts, role: 'user', content: data.text || '', time: formatTime(ts) });
60
+ } else if (event === 'assistant.delta') {
61
+ // Progressive text mid-turn: accumulate into the live bubble; the
62
+ // settled message.append at turn end overwrites with the authoritative
63
+ // full content, so a dropped delta never corrupts the transcript.
64
+ const a = lastAssistant(); if (a && a._live) a.content = (a.content || '') + (data.text || '');
65
+ } else if (event === 'tool.start') {
66
+ const a = lastAssistant(); if (a) (a.parts || (a.parts = [])).push({ kind: 'tool', name: data.name || 'tool', args: data.args || {}, status: 'running', _tcid: data.toolCallId });
67
+ } else if (event === 'tool.end') {
68
+ const a = lastAssistant(); if (a) {
69
+ const p = (a.parts || []).find(p => p.kind === 'tool' && p._tcid === data.toolCallId);
70
+ if (p) {
71
+ p.status = data.denied ? 'error' : 'done';
72
+ p.result = data.denied ? 'denied by user' : (typeof data.result === 'string' ? data.result : JSON.stringify(data.result ?? '', null, 2));
73
+ if (data.denied) p.error = true;
74
+ }
75
+ }
76
+ } else if (event === 'approval.request') {
77
+ const a = lastAssistant(); if (a) (a.parts || (a.parts = [])).push({ kind: 'approval', id: data.id, name: data.name, args: data.args || {}, status: 'pending', onResolve: sendApprove ? (d) => sendApprove(data.id, d) : null });
78
+ } else if (event === 'approval.resolved') {
79
+ const a = lastAssistant(); if (a) {
80
+ const p = (a.parts || []).find(p => p.kind === 'approval' && p.id === data.id);
81
+ if (p) { p.status = data.approved ? 'approved' : 'rejected'; p.always = !!data.always; }
82
+ }
83
+ }
84
+ }
85
+
16
86
  export const chat = makePage((ctx) => {
17
- Object.assign(ctx.state, { loading: false, messages: [], draft: '', busy: false, error: null, abort: null });
87
+ Object.assign(ctx.state, { loading: false, messages: [], draft: '', busy: false, error: null, sessionId: null, ws: null, conn: 'closed' });
18
88
 
19
89
  // Offline outbox: a prompt sent while genuinely offline queues to
20
- // IndexedDB and auto-flushes on the real 'online' event, rather than
21
- // surfacing a hard error the user can't act on. True offline LLM
22
- // response generation is impossible by definition -- a queued message
23
- // only gets a reply once connectivity actually returns. Reconnect-flush
24
- // still goes through the single-shot JSON path (no live UI to stream
25
- // into for a message sent while this page may not even be mounted).
90
+ // IndexedDB and auto-flushes on the real 'online' event via the legacy
91
+ // single-shot POST path (no live UI to stream into for a message sent
92
+ // while this page may not even be mounted).
26
93
  async function sendQueuedToServer(body) {
27
94
  const r = await api('/api/chat', { method: 'POST', body });
28
95
  const reply = r.result || r.content || r.message || (r.messages && r.messages.at(-1)?.content) || JSON.stringify(r);
@@ -31,97 +98,130 @@ export const chat = makePage((ctx) => {
31
98
  }
32
99
  watchReconnect('chat', sendQueuedToServer);
33
100
 
101
+ const s = () => ctx.state;
102
+ const cur = () => s().messages[s().messages.length - 1];
103
+
104
+ function sendFrame(obj) {
105
+ const ws = s().ws;
106
+ if (!ws) return false;
107
+ if (ws.readyState === 1) { ws.send(JSON.stringify(obj)); return true; }
108
+ if (ws.readyState === 0) { ws.addEventListener('open', () => ws.send(JSON.stringify(obj)), { once: true }); return true; }
109
+ return false;
110
+ }
111
+
112
+ function ensureWs() {
113
+ const st = s();
114
+ if (!st.sessionId) st.sessionId = newSessionId();
115
+ if (st.ws && (st.ws.readyState === 1 || st.ws.readyState === 0)) return st.ws;
116
+ try {
117
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
118
+ const ws = new WebSocket(proto + '://' + location.host + '/api/agent/stream?sessionId=' + encodeURIComponent(st.sessionId));
119
+ st.ws = ws;
120
+ ws.onopen = () => { st.conn = 'open'; ctx.rerender(); };
121
+ ws.onmessage = (e) => {
122
+ let f; try { f = JSON.parse(e.data); } catch { return; }
123
+ if (f.type === 'replay') {
124
+ // Rebuild from the server's wire log only when the local
125
+ // thread is empty (first mount / refresh), never clobber a
126
+ // live thread with a reconnect's replay.
127
+ if (!st.messages.length && f.events && f.events.length) {
128
+ const msgs = [];
129
+ for (const env of f.events) applyEnvelope(msgs, env, null);
130
+ // Settled replay: turns are complete, drop the _live marker.
131
+ for (const m of msgs) delete m._live;
132
+ st.messages = msgs;
133
+ }
134
+ ctx.rerender();
135
+ } else if (f.type === 'event') {
136
+ applyEnvelope(st.messages, f, (id, d) => sendFrame({ type: 'approve', id, approved: d.approved, always: !!d.always }));
137
+ ctx.rerender();
138
+ } else if (f.type === 'prompt.done') {
139
+ const c = cur();
140
+ if (c && c.role === 'assistant') {
141
+ delete c._live;
142
+ if (f.error && !c.error) c.error = f.error;
143
+ }
144
+ ctx.set({ busy: false });
145
+ } else if (f.type === 'error') {
146
+ const c = cur();
147
+ if (c && c.role === 'assistant') c.error = f.error;
148
+ ctx.set({ busy: false });
149
+ }
150
+ };
151
+ ws.onclose = () => {
152
+ st.conn = 'closed';
153
+ if (st.busy) {
154
+ const c = cur();
155
+ if (c && c.role === 'assistant') { delete c._live; c.incomplete = true; }
156
+ ctx.set({ busy: false });
157
+ }
158
+ ctx.rerender();
159
+ };
160
+ ws.onerror = () => { st.conn = 'closed'; };
161
+ return ws;
162
+ } catch { return null; }
163
+ }
164
+
34
165
  async function send(text) {
35
- const t = (typeof text === 'string' ? text : ctx.state.draft || '').trim();
36
- if (!t || ctx.state.busy) return;
166
+ const t = (typeof text === 'string' ? text : s().draft || '').trim();
167
+ if (!t) return;
168
+
169
+ // Mid-turn send = STEER (kimi's steering): injected at the next step
170
+ // boundary instead of starting a parallel turn.
171
+ if (s().busy) {
172
+ if (sendFrame({ type: 'steer', text: t })) {
173
+ s().messages = [...s().messages, { id: 'u' + Date.now(), role: 'user', content: t, time: formatTime(Date.now()) }];
174
+ ctx.set({ draft: '' });
175
+ }
176
+ return;
177
+ }
178
+
37
179
  const userMsg = { id: 'u' + Date.now(), role: 'user', content: t, time: formatTime(Date.now()) };
38
- const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: formatTime(Date.now()), parts: [] };
39
- ctx.state.messages = [...ctx.state.messages, userMsg, curMsg];
180
+ const curMsg = { id: 'a' + (Date.now() + 1), role: 'assistant', content: '', time: formatTime(Date.now()), parts: [], _live: true };
181
+ s().messages = [...s().messages, userMsg, curMsg];
40
182
  ctx.set({ draft: '', busy: true, error: null });
41
183
 
42
184
  if (!isOnline()) {
43
185
  await queueMessage('chat', { prompt: t });
44
- ctx.state.messages = ctx.state.messages.slice(0, -1);
45
- ctx.state.messages.push({ id: curMsg.id, role: 'assistant', content: '(offline -- queued, will send when connection returns)', time: formatTime(Date.now()) });
186
+ s().messages = s().messages.slice(0, -1);
187
+ s().messages.push({ id: curMsg.id, role: 'assistant', content: '(offline -- queued, will send when connection returns)', time: formatTime(Date.now()) });
46
188
  ctx.set({ busy: false });
47
189
  return;
48
190
  }
49
191
 
50
- const ctrl = new AbortController();
51
- ctx.state.abort = ctrl;
52
- const cur = ctx.state.messages[ctx.state.messages.length - 1];
53
- try {
54
- const res = await fetch('/api/chat', {
55
- method: 'POST',
56
- headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
57
- body: JSON.stringify({ prompt: t, sessionId: ctx.state.sessionId || undefined }),
58
- signal: ctrl.signal,
59
- });
60
- if (!res.ok || !res.body) {
61
- const txt = await res.text().catch(() => '');
62
- throw new Error(txt || ('HTTP ' + res.status));
63
- }
64
- let finalMessages = null;
65
- for await (const { event, data } of parseSseStream(res)) {
66
- if (ctrl.signal.aborted) break;
67
- if (event === 'start') {
68
- if (data && data.sessionId) ctx.state.sessionId = data.sessionId;
69
- } else if (event === 'tool_progress') {
70
- cur.parts.push(toolProgressPart(data || {}));
71
- ctx.rerender();
72
- } else if (event === 'message') {
73
- // Buffered per-message events land right before `done` -- accumulate
74
- // rather than rerender per-message; the final rebuild below is O(1)
75
- // extra work and avoids a flurry of rerenders in the same tick.
76
- (finalMessages || (finalMessages = [])).push(data);
77
- } else if (event === 'error') {
78
- cur.error = String((data && data.error) || 'stream error');
79
- ctx.rerender();
80
- } else if (event === 'done') {
81
- if (finalMessages && finalMessages.length) {
82
- cur.parts = partsFromMessages(finalMessages);
83
- // Prefer the settled assistant text as plain content when the
84
- // rebuilt parts carry exactly one md part (the common no-tool-call
85
- // case) -- keeps AgentChat's md-vs-content dedup path simple.
86
- cur.content = '';
87
- } else if (data && data.result) {
88
- cur.content = String(data.result);
89
- }
90
- ctx.rerender();
91
- }
92
- }
93
- } catch (e) {
94
- if (e && e.name === 'AbortError') {
95
- cur.stopped = true;
96
- } else {
97
- cur.error = String(e && e.message || e);
98
- }
99
- } finally {
100
- ctx.state.abort = null;
192
+ if (!ensureWs() || !sendFrame({ type: 'prompt', text: t })) {
193
+ curMsg.error = 'agent workspace connection unavailable';
194
+ delete curMsg._live;
101
195
  ctx.set({ busy: false });
102
196
  }
103
197
  }
104
198
 
105
199
  function stop() {
106
- if (ctx.state.abort) { try { ctx.state.abort.abort(); } catch { /* swallow: already settled */ } }
200
+ sendFrame({ type: 'cancel' });
201
+ const c = cur();
202
+ if (c && c.role === 'assistant') c.stopped = true;
203
+ ctx.rerender();
107
204
  }
108
205
 
109
206
  return () => {
110
- const s = ctx.state;
207
+ const st = s();
111
208
  return h('div', { class: 'fd-chat' },
112
209
  AgentChat({
113
- messages: s.messages,
114
- busy: s.busy,
115
- draft: s.draft,
116
- status: s.busy ? 'streaming…' : 'ready',
210
+ messages: st.messages,
211
+ busy: st.busy,
212
+ draft: st.draft,
213
+ status: st.busy ? 'streaming…' : (st.conn === 'open' ? 'ready' : 'connecting…'),
117
214
  agentName: 'freddie',
118
- placeholder: s.busy ? 'waiting for reply…' : 'message…',
215
+ placeholder: st.busy ? 'steer the turn (or stop)' : 'message…',
119
216
  showMinimap: true,
120
- banners: s.error ? [noteAlert({ kind: 'error', msg: s.error })] : [],
121
- onInput: (v) => { s.draft = v; },
217
+ banners: st.error ? [noteAlert({ kind: 'error', msg: st.error })] : [],
218
+ onInput: (v) => { st.draft = v; },
122
219
  onSend: send,
123
220
  onStop: stop,
124
- onNewChat: () => ctx.set({ messages: [], draft: '', error: null, sessionId: null }),
221
+ onNewChat: () => {
222
+ try { st.ws && st.ws.close(); } catch { /* already closed */ }
223
+ ctx.set({ messages: [], draft: '', error: null, sessionId: null, ws: null, conn: 'closed' });
224
+ },
125
225
  }));
126
226
  };
127
227
  });
@@ -26,18 +26,44 @@ export function createDesktopShell({ root = document.body, wm, registry, brand =
26
26
  const { drawer, drawerClose, drawerGrid } = buildDrawer();
27
27
  const taskbar = buildTaskbar();
28
28
 
29
- const apps = typeof registry.list === 'function' ? registry.list() : [...registry.values()];
30
-
31
- for (const app of apps) {
32
- const { menuBtn, railBtn, tile } = buildAppEntries(app, {
33
- onMenuClick: () => { closeMenu(); openApp(app.id); },
34
- onRailClick: () => openApp(app.id),
35
- onTileClick: () => { closeDrawer(); openApp(app.id); },
36
- });
37
- appsMenu.appendChild(menuBtn);
38
- sideRail.appendChild(railBtn);
39
- drawerGrid.appendChild(tile);
29
+ // The registry is not frozen at shell creation hosts can register and
30
+ // unregister apps later (thebird's per-instance user-* apps come and go on
31
+ // instance switch and fs edits). refreshApps() re-syncs the three launcher
32
+ // surfaces (apps menu / side rail / drawer grid) surgically: entries for
33
+ // newly registered apps are appended, entries whose app was unregistered
34
+ // are removed, and already-rendered entries keep their nodes (listeners,
35
+ // focus, and any host-side regrouping of the menu intact). The initial
36
+ // render is the same call. Buttons are tagged data-app-id so removal can
37
+ // find them again.
38
+ const renderedAppIds = new Set();
39
+ function refreshApps() {
40
+ const apps = typeof registry.list === 'function' ? registry.list() : [...registry.values()];
41
+ const live = new Set(apps.map(a => a.id));
42
+ for (const id of renderedAppIds) {
43
+ if (live.has(id)) continue;
44
+ for (const container of [appsMenu, sideRail, drawerGrid]) {
45
+ const stale = container.querySelector('[data-app-id="' + id + '"]');
46
+ if (stale) stale.remove();
47
+ }
48
+ renderedAppIds.delete(id);
49
+ }
50
+ for (const app of apps) {
51
+ if (renderedAppIds.has(app.id)) continue;
52
+ const { menuBtn, railBtn, tile } = buildAppEntries(app, {
53
+ onMenuClick: () => { closeMenu(); openApp(app.id); },
54
+ onRailClick: () => openApp(app.id),
55
+ onTileClick: () => { closeDrawer(); openApp(app.id); },
56
+ });
57
+ menuBtn.dataset.appId = app.id;
58
+ railBtn.dataset.appId = app.id;
59
+ tile.dataset.appId = app.id;
60
+ appsMenu.appendChild(menuBtn);
61
+ sideRail.appendChild(railBtn);
62
+ drawerGrid.appendChild(tile);
63
+ renderedAppIds.add(app.id);
64
+ }
40
65
  }
66
+ refreshApps();
41
67
 
42
68
  osRoot.append(menubar, appsMenu, taskbar);
43
69
  document.body.append(sideRail, drawer);
@@ -193,7 +219,11 @@ export function createDesktopShell({ root = document.body, wm, registry, brand =
193
219
  refreshTaskbar();
194
220
  const finish = (r) => {
195
221
  if (isAsync && typeof win.setBody === 'function') win.setBody(r.node);
196
- win._app = { id: appId, dispose: r.dispose };
222
+ // Keep the FULL factory result on _app (only id is overridden with
223
+ // the registry's appId): hosts persist/restore per-window view
224
+ // state through getViewState/restoreViewState hooks on the factory
225
+ // result — a lossy {id, dispose} wrap silently dropped them.
226
+ win._app = { ...r, id: appId };
197
227
  refreshTaskbar();
198
228
  return win;
199
229
  };
@@ -210,7 +240,7 @@ export function createDesktopShell({ root = document.body, wm, registry, brand =
210
240
 
211
241
  const api = {
212
242
  wm, registry, openApp, setContext, refreshTaskbar, setActiveInstance,
213
- openDrawer, closeDrawer, openMenu, closeMenu,
243
+ openDrawer, closeDrawer, openMenu, closeMenu, refreshApps,
214
244
  get activeInstanceId() { return activeInstanceId; },
215
245
  elements: { osRoot, menubar, taskbar, appsMenu, sideRail, drawer, instSwitch, homeBtn, appsBtn },
216
246
  dispose() { clearInterval(clockTimer); clearInterval(taskTimer); window.removeEventListener('resize', onViewportResize); osRoot.remove(); sideRail.remove(); drawer.remove(); },