anentrypoint-design 0.0.329 → 0.0.331

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.329",
3
+ "version": "0.0.331",
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",
@@ -130,6 +130,15 @@ export function injectCodeCopy(container) {
130
130
  const MD_STREAM_THROTTLE_MS = 120;
131
131
  const MD_STREAM_MIN_DELTA_CHARS = 40;
132
132
 
133
+ // requestIdleCallback with a setTimeout fallback (Safari/non-browser test
134
+ // contexts lack the real API). A settled historical message's parse is not
135
+ // latency-critical the way a streaming turn's is — deferring it off the
136
+ // critical path keeps a session-load burst of N historical bubbles from
137
+ // racing N synchronous parses on the same tick.
138
+ const scheduleIdle = typeof requestIdleCallback === 'function'
139
+ ? (fn) => requestIdleCallback(fn, { timeout: 500 })
140
+ : (fn) => setTimeout(fn, 0);
141
+
133
142
  function MdNode(p) {
134
143
  const refSink = (el) => {
135
144
  if (!el) return;
@@ -164,19 +173,34 @@ function MdNode(p) {
164
173
  // arrive (an empty bubble until the CDN import resolves reads as a
165
174
  // hang); the resolved render swaps in sanitized markdown in place.
166
175
  if (isMarkdownDegraded()) el.textContent = p.text || '';
167
- renderMarkdownCached(p.text || '').then((html) => {
168
- const swap = () => { el.innerHTML = html; injectCodeCopy(el); };
169
- // Don't blow away an active text selection inside this bubble mid-swap
170
- // (e.g. the user is mid-copy while a stream tick settles). Defer the
171
- // swap once, until the selection changes (cleared or moved elsewhere).
172
- const sel = typeof window !== 'undefined' ? window.getSelection() : null;
173
- if (sel && sel.anchorNode && el.contains(sel.anchorNode)) {
174
- const onSelChange = () => { document.removeEventListener('selectionchange', onSelChange); swap(); };
175
- document.addEventListener('selectionchange', onSelChange, { once: true });
176
- return;
177
- }
178
- swap();
179
- });
176
+ function doParse() {
177
+ renderMarkdownCached(p.text || '').then((html) => {
178
+ // The element may have been recycled (webjsx applyDiff reused this
179
+ // DOM node for a different message) or detached by the time an
180
+ // idle-deferred parse resolves -- re-check the source key still
181
+ // matches before swapping innerHTML into what could now be a
182
+ // completely different message's bubble.
183
+ if (el.dataset.mdSrc !== srcKey) return;
184
+ const swap = () => { el.innerHTML = html; injectCodeCopy(el); };
185
+ // Don't blow away an active text selection inside this bubble mid-swap
186
+ // (e.g. the user is mid-copy while a stream tick settles). Defer the
187
+ // swap once, until the selection changes (cleared or moved elsewhere).
188
+ const sel = typeof window !== 'undefined' ? window.getSelection() : null;
189
+ if (sel && sel.anchorNode && el.contains(sel.anchorNode)) {
190
+ const onSelChange = () => { document.removeEventListener('selectionchange', onSelChange); swap(); };
191
+ document.addEventListener('selectionchange', onSelChange, { once: true });
192
+ return;
193
+ }
194
+ swap();
195
+ });
196
+ }
197
+ // Streaming turns parse immediately (latency-critical: the user is
198
+ // watching this bubble grow). A settled historical message (no
199
+ // streamingCaret) is deferred to an idle slot -- not on the critical
200
+ // render path, so a page mounting many historical bubbles at once
201
+ // (session load) doesn't burst-parse them all synchronously.
202
+ if (p.streamingCaret) doParse();
203
+ else scheduleIdle(doParse);
180
204
  };
181
205
  return h('div', { class: 'chat-bubble chat-md', ref: refSink });
182
206
  }
@@ -44,6 +44,7 @@ export function makePage(setup, { initial = {} } = {}) {
44
44
  return function pageRenderer(host) {
45
45
  const state = { loading: true, error: null, ...initial };
46
46
  const timers = [];
47
+ const cleanupFns = [];
47
48
  let elRef = null;
48
49
  let render = () => h('div', {});
49
50
  const ctx = {
@@ -51,7 +52,16 @@ export function makePage(setup, { initial = {} } = {}) {
51
52
  set(patch) { Object.assign(state, patch); ctx.rerender(); },
52
53
  rerender() { if (elRef) { try { applyDiff(elRef, wrap()); } catch (e) { console.warn('[freddie page rerender]', e); } } },
53
54
  interval(fn, ms) { const id = setInterval(fn, ms); timers.push(id); return id; },
54
- cleanup() { for (const id of timers) clearInterval(id); timers.length = 0; },
55
+ // Register arbitrary teardown (WebSocket close, event listener removal,
56
+ // etc) alongside the existing interval-only cleanup() -- same
57
+ // unmount trigger (elRef going null in ref()), just not limited to
58
+ // setInterval ids.
59
+ onCleanup(fn) { cleanupFns.push(fn); },
60
+ cleanup() {
61
+ for (const id of timers) clearInterval(id); timers.length = 0;
62
+ for (const fn of cleanupFns) { try { fn(); } catch (e) { console.warn('[freddie page cleanup]', e); } }
63
+ cleanupFns.length = 0;
64
+ },
55
65
  };
56
66
  function wrap() {
57
67
  let body;
@@ -10,6 +10,7 @@ import { getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from '
10
10
  import { Panel, Row, Table, Kpi, PageHeader, SearchInput, TextField, Select } from './content.js';
11
11
  import { Chip, Btn, Icon } from './shell.js';
12
12
  import { formatTime } from '../locale.js';
13
+ import { register as registerDebug, unregister as unregisterDebug } from '../debug.js';
13
14
  import { queueMessage, watchReconnect, isOnline } from '../idb-outbox.js';
14
15
  import { ChatMessage, ChatComposer } from './chat.js';
15
16
  import { fmtTime, fmtAgo } from './sessions.js';
@@ -704,6 +705,98 @@ export const health = makePage((ctx) => {
704
705
 
705
706
  // ---- debug -----------------------------------------------------------------
706
707
 
708
+ // ---- logs --------------------------------------------------------------
709
+
710
+ const LOG_SEVERITY_TONE = { error: 'error', warning: 'warning', info: 'accent', debug: 'muted' };
711
+
712
+ export const logs = makePage((ctx) => {
713
+ Object.assign(ctx.state, { lines: [], subsystems: [], activeSubsystem: '', activeSeverity: '', q: '', connected: false, wsError: null });
714
+ const MAX_LINES = 500;
715
+
716
+ async function loadSubsystems() {
717
+ try { ctx.set({ subsystems: await api('/api/logs') }); }
718
+ catch (e) { /* non-fatal: subsystem list is a filter convenience, not required for the stream */ }
719
+ }
720
+
721
+ let unmounted = false;
722
+ let reconnectTimer = null;
723
+ function connect() {
724
+ if (unmounted) return;
725
+ if (typeof WebSocket === 'undefined') { ctx.set({ wsError: new Error('WebSocket not available in this environment') }); return }
726
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
727
+ const ws = new WebSocket(proto + '//' + location.host + '/api/logs/stream');
728
+ ws.onopen = () => ctx.set({ connected: true, wsError: null });
729
+ ws.onerror = () => ctx.set({ connected: false, wsError: new Error('log stream connection error') });
730
+ ws.onclose = () => { ctx.set({ connected: false }); if (!unmounted) reconnectTimer = setTimeout(connect, 3000); };
731
+ ws.onmessage = (ev) => {
732
+ let rec; try { rec = JSON.parse(ev.data); } catch { return; }
733
+ const next = [rec, ...ctx.state.lines].slice(0, MAX_LINES);
734
+ ctx.set({ lines: next });
735
+ };
736
+ currentWs = ws;
737
+ }
738
+ let currentWs = null;
739
+
740
+ loadSubsystems();
741
+ connect();
742
+ registerDebug('logs', () => ({ connected: ctx.state.connected, lineCount: ctx.state.lines.length, activeSubsystem: ctx.state.activeSubsystem, activeSeverity: ctx.state.activeSeverity }));
743
+ ctx.onCleanup(() => {
744
+ unmounted = true;
745
+ if (reconnectTimer) clearTimeout(reconnectTimer);
746
+ try { currentWs?.close(); } catch {}
747
+ unregisterDebug('logs');
748
+ });
749
+
750
+ function filtered() {
751
+ const s = ctx.state;
752
+ return s.lines.filter((l) => {
753
+ if (s.activeSubsystem && l.subsystem !== s.activeSubsystem) return false;
754
+ if (s.activeSeverity && l.severity !== s.activeSeverity) return false;
755
+ if (s.q && !String(l.msg || '').toLowerCase().includes(s.q.toLowerCase())) return false;
756
+ return true;
757
+ });
758
+ }
759
+
760
+ return () => {
761
+ const s = ctx.state;
762
+ const rows = filtered();
763
+ const severities = ['error', 'warning', 'info', 'debug'];
764
+ return [
765
+ PageHeader({
766
+ eyebrow: 'freddie', title: 'logs', lede: 'live JSONL log tail — /api/logs/stream',
767
+ right: s.connected ? Chip({ tone: 'ok', children: 'live' }) : Chip({ tone: 'miss', children: 'reconnecting…' }),
768
+ }),
769
+ s.wsError ? refreshError(s.wsError) : null,
770
+ h('div', { class: 'ds-toolbar' },
771
+ SearchInput({ value: s.q, placeholder: 'filter by message…', onInput: (v) => ctx.set({ q: v }), resultCount: rows.length }),
772
+ Select({
773
+ label: 'subsystem', value: s.activeSubsystem, placeholder: 'all subsystems',
774
+ options: (s.subsystems || []).map((name) => ({ value: name, label: name })),
775
+ onChange: (v) => ctx.set({ activeSubsystem: v }),
776
+ }),
777
+ Select({
778
+ label: 'severity', value: s.activeSeverity, placeholder: 'all severities',
779
+ options: severities.map((sv) => ({ value: sv, label: sv })),
780
+ onChange: (v) => ctx.set({ activeSeverity: v }),
781
+ }),
782
+ ),
783
+ rows.length ? section('lines · ' + rows.length,
784
+ Table({
785
+ headers: ['time', 'subsystem', 'severity', 'message'],
786
+ rows: rows.map((l) => [
787
+ formatTime(l.ts ? Date.parse(l.ts) : Date.now()),
788
+ l.subsystem || '—',
789
+ Chip({ tone: LOG_SEVERITY_TONE[l.severity] || 'dim', children: l.severity || 'info' }),
790
+ truncSpan(l.msg, TRUNC_DESC),
791
+ ]),
792
+ }),
793
+ ) : emptyState(s.connected ? 'no log lines yet — waiting for activity' : 'connecting to log stream…'),
794
+ ].filter(Boolean);
795
+ };
796
+ });
797
+
798
+ // ---- debug -----------------------------------------------------------------
799
+
707
800
  export const debug = makePage((ctx) => {
708
801
  Object.assign(ctx.state, { sub: null, logs: null });
709
802
  async function load() { try { ctx.set({ loading: false, data: await api('/api/debug'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
@@ -734,7 +827,7 @@ export const debug = makePage((ctx) => {
734
827
  export const FREDDIE_PAGES = {
735
828
  home, chat, voice, sessions, projects, agents, analytics,
736
829
  models, cron, skills, config, env, tools, batch, gateway, chains,
737
- machines, health, debug,
830
+ machines, health, debug, logs,
738
831
  };
739
832
 
740
833
  export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages };