The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit b95ad82c1c50Commit detailsView latest

apps/dashboard/src/main.tsx 3257 lines · 115.2 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import { ArrowRight, Bot, Moon, Sun } from 'lucide-react';
4
import { AppShell } from '@/components/app-shell';
5
import { ClaspMark } from '@/components/clasp-mark';
6
import { LandingBackdrop, LandingClock, LandingDiagram } from '@/components/landing-scene';
7
import { Input } from '@/components/ui/input';
8
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
9
import { pageMeta, pages, type Page } from '@/lib/navigation';
10
import {
11
  applyPreviewRequest,
12
  createPreviewData,
13
  defaultPreviewSettings,
14
  disablePreview,
15
  enablePreview,
16
  isPreviewActive,
17
  previewSession,
18
} from './preview.js';
19
import './styles.css';
20
21
declare const __AUTH0_DOMAIN__: string;
22
declare const __AUTH0_CLIENT_ID__: string;
23
declare const __AUTH0_AUDIENCE__: string;
24
25
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
26
type AuthSession = { user: Auth0User };
27
type AuthTransaction = { state: string; nonce: string; verifier: string };
28
type Theme = 'dark' | 'light';
29
30
const authTransactionKey = 'openclasp.auth0.transaction';
31
const themeKey = 'openclasp.theme.v1';
32
const landingCapabilities = [
33
  'identity',
34
  'discovery',
35
  'counterparty',
36
  'contracts',
37
  'monitoring',
38
  'outcomes',
39
  'feedback',
40
];
41
42
type DashboardData = {
43
  agents: Record<string, any>[];
44
  projects: Record<string, any>[];
45
  installations: Record<string, any>[];
46
  setupRequests: Record<string, any>[];
47
  publications: Record<string, any>[];
48
  interactions: Record<string, any>[];
49
  federatedInteractions: Record<string, any>[];
50
  liveSessions: Record<string, any>[];
51
  hostedThreads: Record<string, any>[];
52
  events: Record<string, any>[];
53
  conflicts: Record<string, any>[];
54
  receipts: Record<string, any>[];
55
  profiles: Record<string, any>[];
56
  counterpartyBriefs: Record<string, any>[];
57
  completionReports: Record<string, any>[];
58
  feedbackRequests: Record<string, any>[];
59
  interactionFeedback: Record<string, any>[];
60
  interactionConclusions: Record<string, any>[];
61
  learningEligibility: Record<string, any>[];
62
  profileDeltas: Record<string, any>[];
63
  runtimes: Record<string, any>[];
64
  accessTokens: Record<string, any>[];
65
};
66
67
type Settings = {
68
  displayName: string;
69
  contributionEnabled: boolean;
70
  retentionDays: number;
71
  evidenceSharing: 'never' | 'ask' | 'contract_only';
72
  rawConversationsStored: false;
73
};
74
75
const emptyData: DashboardData = {
76
  agents: [],
77
  projects: [],
78
  installations: [],
79
  setupRequests: [],
80
  publications: [],
81
  interactions: [],
82
  federatedInteractions: [],
83
  liveSessions: [],
84
  hostedThreads: [],
85
  events: [],
86
  conflicts: [],
87
  receipts: [],
88
  profiles: [],
89
  counterpartyBriefs: [],
90
  completionReports: [],
91
  feedbackRequests: [],
92
  interactionFeedback: [],
93
  interactionConclusions: [],
94
  learningEligibility: [],
95
  profileDeltas: [],
96
  runtimes: [],
97
  accessTokens: [],
98
};
99
const defaultSettings: Settings = {
100
  displayName: '',
101
  contributionEnabled: false,
102
  retentionDays: 30,
103
  evidenceSharing: 'ask',
104
  rawConversationsStored: false,
105
};
106
107
function normalizeDashboard(value: unknown): DashboardData {
108
  return { ...emptyData, ...(value as Partial<DashboardData>) };
109
}
110
111
function base64Url(bytes: Uint8Array) {
112
  let binary = '';
113
  for (const byte of bytes) binary += String.fromCharCode(byte);
114
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
115
}
116
117
function randomValue(size = 32) {
118
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
119
}
120
121
async function beginAuth(provider: 'google' | 'github') {
122
  const verifier = randomValue(48);
123
  const digest = await crypto.subtle.digest(
124
    'SHA-256',
125
    new TextEncoder().encode(verifier) as BufferSource,
126
  );
127
  const transaction: AuthTransaction = { state: randomValue(), nonce: randomValue(), verifier };
128
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
129
  const parameters = new URLSearchParams({
130
    client_id: __AUTH0_CLIENT_ID__,
131
    redirect_uri: `${new URL(__AUTH0_AUDIENCE__).origin}/sso-callback`,
132
    response_type: 'code',
133
    scope: 'openid profile email',
134
    audience: __AUTH0_AUDIENCE__,
135
    state: transaction.state,
136
    nonce: transaction.nonce,
137
    code_challenge: base64Url(new Uint8Array(digest)),
138
    code_challenge_method: 'S256',
139
    connection: provider === 'google' ? 'google-oauth2' : 'github',
140
  });
141
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
142
}
143
144
async function signOut(preview: boolean) {
145
  if (preview) {
146
    disablePreview();
147
    location.replace('/login');
148
    return;
149
  }
150
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
151
  const parameters = new URLSearchParams({
152
    client_id: __AUTH0_CLIENT_ID__,
153
    returnTo: `${new URL(__AUTH0_AUDIENCE__).origin}/login`,
154
  });
155
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
156
}
157
158
async function remoteApi(path: string, init?: RequestInit) {
159
  const headers = new Headers(init?.headers);
160
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
161
  return fetch(path, {
162
    ...init,
163
    credentials: 'same-origin',
164
    headers,
165
  }).then(async (response) => {
166
    if (!response.ok)
167
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
168
    return response.json();
169
  });
170
}
171
172
function route(): Page {
173
  const value = location.pathname.slice(1) || 'dashboard';
174
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
175
}
176
177
function initialTheme(): Theme {
178
  const saved = localStorage.getItem(themeKey);
179
  if (saved === 'dark' || saved === 'light') return saved;
180
  return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
181
}
182
183
function App() {
184
  const [session, setSession] = useState<AuthSession | null>();
185
  const [preview, setPreview] = useState(false);
186
  const [theme, setTheme] = useState<Theme>(initialTheme);
187
  const [page, setPage] = useState<Page>(route());
188
  const [data, setData] = useState<DashboardData>(emptyData);
189
  const [settings, setSettings] = useState<Settings>(defaultSettings);
190
  const [loading, setLoading] = useState(true);
191
  const [error, setError] = useState('');
192
  const api = useCallback(
193
    async (path: string, init?: RequestInit) => {
194
      if (preview) {
195
        const next = applyPreviewRequest(data, settings, path, init);
196
        setData(next.data);
197
        setSettings(next.settings);
198
        return next.result;
199
      }
200
      return remoteApi(path, init);
201
    },
202
    [data, preview, settings],
203
  );
204
  const refreshDashboard = useCallback(async () => {
205
    if (preview) return;
206
    setData(normalizeDashboard(await remoteApi('/v0.1/dashboard')));
207
  }, [preview]);
208
209
  useEffect(() => {
210
    document.title = session
211
      ? `${pageMeta[page].title} · OpenClasp`
212
      : session === null
213
        ? 'OpenClasp · Trust for agent communication'
214
        : 'OpenClasp';
215
  }, [page, session]);
216
217
  useEffect(() => {
218
    let cancelled = false;
219
    const boot = async () => {
220
      if (import.meta.env.DEV && isPreviewActive()) {
221
        if (cancelled) return;
222
        setPreview(true);
223
        setSession(previewSession);
224
        setData(createPreviewData());
225
        setSettings(defaultPreviewSettings);
226
        setLoading(false);
227
        return;
228
      }
229
      try {
230
        const response = await fetch('/api/session', { credentials: 'same-origin' });
231
        if (!response.ok) {
232
          if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
233
          return;
234
        }
235
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) {
236
          setSession((await response.json()) as AuthSession);
237
        }
238
      } catch {
239
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
240
      }
241
    };
242
    void boot();
243
    return () => {
244
      cancelled = true;
245
    };
246
  }, []);
247
248
  useEffect(() => {
249
    document.documentElement.dataset.theme = theme;
250
    document.documentElement.classList.toggle('dark', theme === 'dark');
251
    localStorage.setItem(themeKey, theme);
252
    const meta = document.querySelector('meta[name="theme-color"]');
253
    if (meta) meta.setAttribute('content', theme === 'light' ? '#f7f4f2' : '#0c0a0a');
254
  }, [theme]);
255
256
  useEffect(() => {
257
    const onPopState = () => setPage(route());
258
    addEventListener('popstate', onPopState);
259
    return () => removeEventListener('popstate', onPopState);
260
  }, []);
261
262
  useEffect(() => {
263
    if (session === undefined) return;
264
    if (!session && location.pathname !== '/login') history.replaceState({}, '', '/login');
265
    if (!session) return;
266
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
267
    setPage(route());
268
    if (preview) {
269
      setLoading(false);
270
      return;
271
    }
272
    Promise.all([remoteApi('/v0.1/dashboard'), remoteApi('/v0.1/settings')])
273
      .then(([dashboard, accountSettings]) => {
274
        setData(normalizeDashboard(dashboard));
275
        setSettings(accountSettings as Settings);
276
      })
277
      .catch((reason: unknown) =>
278
        setError(reason instanceof Error ? reason.message : 'Load failed'),
279
      )
280
      .finally(() => setLoading(false));
281
  }, [preview, session]);
282
283
  useEffect(() => {
284
    if (!session || preview) return;
285
    const refresh = () => {
286
      if (document.visibilityState === 'visible') void refreshDashboard().catch(() => undefined);
287
    };
288
    const timer = window.setInterval(refresh, 10_000);
289
    document.addEventListener('visibilitychange', refresh);
290
    return () => {
291
      window.clearInterval(timer);
292
      document.removeEventListener('visibilitychange', refresh);
293
    };
294
  }, [preview, refreshDashboard, session]);
295
296
  const navigate = (next: Page) => {
297
    history.pushState({}, '', `/${next}`);
298
    setPage(next);
299
  };
300
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
301
  const pendingInvites = data.federatedInteractions.filter(
302
    (interaction) => interaction.status === 'pending',
303
  ).length;
304
  const unreadThreads = data.hostedThreads.reduce(
305
    (total, thread) => total + Number(thread.unreadCount ?? 0),
306
    0,
307
  );
308
309
  if (session === undefined) return <Loading />;
310
  if (!session)
311
    return import.meta.env.DEV ? (
312
      <Login
313
        theme={theme}
314
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
315
        onPreview={() => {
316
          enablePreview();
317
          setPreview(true);
318
          setSession(previewSession);
319
          setData(createPreviewData());
320
          setSettings(defaultPreviewSettings);
321
          setLoading(false);
322
          history.replaceState({}, '', '/dashboard');
323
          setPage('dashboard');
324
        }}
325
      />
326
    ) : (
327
      <Login
328
        theme={theme}
329
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
330
      />
331
    );
332
333
  return (
334
    <AppShell
335
      page={page}
336
      navigate={navigate}
337
      user={session.user}
338
      theme={theme}
339
      onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
340
      onSignOut={() => void signOut(preview)}
341
      badges={{
342
        dashboard: pendingInvites || pendingSetup ? pendingInvites + pendingSetup : 0,
343
        conversations: unreadThreads,
344
        connect: pendingSetup,
345
      }}
346
      agents={data.agents.map((agent) => ({
347
        agentId: String(agent.agentId ?? ''),
348
        name: typeof agent.name === 'string' ? agent.name : undefined,
349
      }))}
350
      attention={{ setup: pendingSetup, invites: pendingInvites, inbox: unreadThreads }}
351
    >
352
      {preview && <div className="previewBanner">local preview · changes stay here</div>}
353
      {error && (
354
        <div className="errorBar" role="alert">
355
          {error}
356
        </div>
357
      )}
358
      {loading ? (
359
        <Loading compact />
360
      ) : (
361
        <PageContent
362
          page={page}
363
          data={data}
364
          settings={settings}
365
          setSettings={setSettings}
366
          navigate={navigate}
367
          refreshDashboard={refreshDashboard}
368
          api={api}
369
        />
370
      )}
371
    </AppShell>
372
  );
373
}
374
375
function Login({
376
  onPreview,
377
  theme,
378
  onToggleTheme,
379
}: {
380
  onPreview?: () => void;
381
  theme: Theme;
382
  onToggleTheme: () => void;
383
}) {
384
  const [error, setError] = useState('');
385
  const continueWith = async (provider: 'google' | 'github') => {
386
    setError('');
387
    try {
388
      await beginAuth(provider);
389
    } catch {
390
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
391
    }
392
  };
393
  const scrollToAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => {
394
    const id = event.currentTarget.hash.slice(1);
395
    const target = document.getElementById(id);
396
    if (!target) return;
397
    event.preventDefault();
398
    const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
399
    history.pushState({}, '', `#${id}`);
400
    target.classList.remove('is-arriving');
401
    if (!reducedMotion && id !== 'top') {
402
      void target.offsetWidth;
403
      target.classList.add('is-arriving');
404
      window.setTimeout(() => target.classList.remove('is-arriving'), 900);
405
    }
406
    target.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'start' });
407
  };
408
  return (
409
    <div className="landingPage">
410
      <LandingBackdrop />
411
      <header className="landingNav">
412
        <a
413
          className="landingBrand"
414
          href="#top"
415
          aria-label="OpenClasp home"
416
          onClick={scrollToAnchor}
417
        >
418
          <ClaspMark className="landingMark" size={22} />
419
          <strong>openclasp</strong>
420
        </a>
421
        <nav aria-label="Main navigation">
422
          <a href="#product" onClick={scrollToAnchor}>
423
            layer
424
          </a>
425
          <a href="#principles" onClick={scrollToAnchor}>
426
            bounds
427
          </a>
428
        </nav>
429
        <div className="landingNavActions">
430
          <LandingClock />
431
          <button
432
            className="themeButton"
433
            type="button"
434
            onClick={onToggleTheme}
435
            aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
436
          >
437
            {theme === 'dark' ? <Sun /> : <Moon />}
438
          </button>
439
          <a className="navCta" href="#access" onClick={scrollToAnchor}>
440
            connect <ArrowRight />
441
          </a>
442
        </div>
443
      </header>
444
445
      <main id="top">
446
        <section className="landingHero">
447
          <div className="heroCopy">
448
            <p className="landingKicker">
449
              <span>//</span> protocol for agent-to-agent trust
450
            </p>
451
            <h1>
452
              Let agents talk.
453
              <br />
454
              <em>Know who to trust.</em>
455
            </h1>
456
            <p>
457
              OpenClasp adds verified identity, negotiated contracts, and evidence-backed history to
458
              direct agent-to-agent communication.
459
            </p>
460
            <div className="heroActions">
461
              <a className="landingPrimary" href="#access" onClick={scrollToAnchor}>
462
                connect <ArrowRight />
463
              </a>
464
              <a className="landingSecondary" href="#product" onClick={scrollToAnchor}>
465
                read the layer
466
              </a>
467
            </div>
468
            <div className="heroNotes" aria-label="Protocol flags">
469
              <span>--direct-a2a</span>
470
              <span>--evidence-only</span>
471
              <span>--no-score</span>
472
            </div>
473
          </div>
474
          <LandingDiagram />
475
        </section>
476
477
        <div className="capabilityRail" aria-label="OpenClasp capabilities">
478
          {landingCapabilities.map((item) => (
479
            <span key={item}>{item}</span>
480
          ))}
481
        </div>
482
483
        <section className="productSection" id="product">
484
          <div className="sectionIntro">
485
            <p className="landingKicker">
486
              <span>01</span> before, during, after
487
            </p>
488
            <h2>
489
              A trust layer.
490
              <br />
491
              <em>Not another transport.</em>
492
            </h2>
493
            <p>
494
              Agents keep communicating over A2A. OpenClasp makes each counterparty and interaction
495
              easier to inspect, constrain, and learn from.
496
            </p>
497
          </div>
498
          <div className="recordAsk" aria-label="Questions OpenClasp makes inspectable">
499
            <span>? query</span>
500
            <p>who is the counterparty</p>
501
            <p>what did both sides sign</p>
502
            <p>what does the evidence show</p>
503
          </div>
504
          <ol className="recordSteps">
505
            <li>
506
              <span>before</span>
507
              <div>
508
                <h3>Find the right counterparty.</h3>
509
                <p>Public discovery meets private, task-specific insight from your own history.</p>
510
                <pre className="asciiMini">{`[A] atlas     verified · research
511
[K] keel      3 outcomes · ops`}</pre>
512
              </div>
513
            </li>
514
            <li>
515
              <span>agree</span>
516
              <div>
517
                <h3>Negotiate the work.</h3>
518
                <p>Agents propose, revise, and sign the same hash before anything starts.</p>
519
                <pre className="asciiMini">{`SCOPE     source-backed brief
520
SHARING   participants only
521
HASH      0x7F3C · bilateral`}</pre>
522
              </div>
523
            </li>
524
            <li>
525
              <span>after</span>
526
              <div>
527
                <h3>Keep usable history.</h3>
528
                <p>Monitoring, outcomes, and feedback stay attached. Trust never becomes one score.</p>
529
                <pre className="asciiMini">{`09:41  + contract signed
530
10:08  + outcome recorded
531
10:12  · feedback attached`}</pre>
532
              </div>
533
            </li>
534
          </ol>
535
        </section>
536
537
        <section className="principlesSection" id="principles">
538
          <div>
539
            <p className="landingKicker">
540
              <span>02</span> deliberately bounded
541
            </p>
542
            <h2>
543
              Trust should be
544
              <br />
545
              <em>specific and inspectable.</em>
546
            </h2>
547
          </div>
548
          <div className="principleList">
549
            <article>
550
              <span>MUST NOT</span>
551
              <div>
552
                <h3>provide the agent</h3>
553
                <p>OpenClasp works alongside agents. It does not provide them.</p>
554
              </div>
555
            </article>
556
            <article>
557
              <span>MUST NOT</span>
558
              <div>
559
                <h3>own the transport</h3>
560
                <p>Communication remains direct over A2A.</p>
561
              </div>
562
            </article>
563
            <article>
564
              <span>MUST NOT</span>
565
              <div>
566
                <h3>collapse trust to a score</h3>
567
                <p>Insight stays contextual, evidence-backed, and useful for the task at hand.</p>
568
              </div>
569
            </article>
570
          </div>
571
        </section>
572
573
        <section className="accessSection" id="access">
574
          <div className="accessCopy">
575
            <p className="landingKicker">
576
              <span>03</span> identity required
577
            </p>
578
            <h2>Make every agent interaction accountable.</h2>
579
            <p>
580
              Sign in to connect agents, control network participation, and inspect signed history.
581
            </p>
582
          </div>
583
          <div className="accessCard">
584
            <div className="accessChrome">
585
              <span>$ authenticate</span>
586
              <span className="accessBlink">_</span>
587
            </div>
588
            <div className="socialButtons">
589
              <button type="button" onClick={() => void continueWith('google')}>
590
                <GoogleMark /> continue with google
591
              </button>
592
              <button type="button" onClick={() => void continueWith('github')}>
593
                <GitHubMark /> continue with github
594
              </button>
595
              {onPreview && (
596
                <button type="button" onClick={() => void onPreview()}>
597
                  open local preview <ArrowRight />
598
                </button>
599
              )}
600
            </div>
601
            {error && (
602
              <div className="loginError" role="alert">
603
                {error}
604
              </div>
605
            )}
606
            <small>google or github · openclasp never receives your password</small>
607
          </div>
608
        </section>
609
      </main>
610
611
      <footer className="landingFooter">
612
        <a className="landingBrand" href="#top" onClick={scrollToAnchor}>
613
          <ClaspMark className="landingMark" size={22} />
614
          <strong>openclasp</strong>
615
        </a>
616
        <p>assurance for open agent communication.</p>
617
        <small>
618
          Not an agent provider, proprietary transport, blockchain product, or universal trust
619
          score.
620
        </small>
621
      </footer>
622
    </div>
623
  );
624
}
625
626
let callbackStarted = false;
627
function AuthCallback() {
628
  const [error, setError] = useState('');
629
  useEffect(() => {
630
    if (callbackStarted) return;
631
    callbackStarted = true;
632
    const finish = async () => {
633
      const query = new URLSearchParams(location.search);
634
      const oauthError = query.get('error_description') ?? query.get('error');
635
      if (oauthError) throw new Error(oauthError);
636
      const code = query.get('code');
637
      const returnedState = query.get('state');
638
      if (code && returnedState?.startsWith('oc_tx_')) {
639
        const response = await fetch('/api/oauth-callback', {
640
          method: 'POST',
641
          credentials: 'same-origin',
642
          headers: { 'content-type': 'application/json' },
643
          body: JSON.stringify({ code, state: returnedState }),
644
        });
645
        if (!response.ok) throw new Error('MCP authorization failed');
646
        const result = (await response.json()) as { redirectTo?: unknown };
647
        if (typeof result.redirectTo !== 'string')
648
          throw new Error('Invalid MCP authorization response');
649
        location.replace(result.redirectTo);
650
        return;
651
      }
652
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
653
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
654
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
655
      sessionStorage.removeItem(authTransactionKey);
656
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
657
      const response = await fetch('/api/session', {
658
        method: 'POST',
659
        credentials: 'same-origin',
660
        headers: { 'content-type': 'application/json' },
661
        body: JSON.stringify({
662
          code,
663
          codeVerifier: transaction.verifier,
664
        }),
665
      });
666
      if (!response.ok) throw new Error('Auth0 token exchange failed');
667
      location.replace('/dashboard');
668
    };
669
    void finish().catch((reason: unknown) =>
670
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
671
    );
672
  }, []);
673
  if (error)
674
    return (
675
      <div className="loading">
676
        <span className="mark">OC</span>
677
        <p>{error}</p>
678
        <a href="/login">Return to login</a>
679
      </div>
680
    );
681
  return <Loading />;
682
}
683
684
function PageContent({
685
  page,
686
  data,
687
  settings,
688
  setSettings,
689
  navigate,
690
  refreshDashboard,
691
  api,
692
}: {
693
  page: Page;
694
  data: DashboardData;
695
  settings: Settings;
696
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
697
  navigate: (page: Page) => void;
698
  refreshDashboard: () => Promise<void>;
699
  api: (path: string, init?: RequestInit) => Promise<unknown>;
700
}) {
701
  if (page === 'history') return <History data={data} />;
702
  if (page === 'conversations')
703
    return <Conversations data={data} refreshDashboard={refreshDashboard} api={api} />;
704
  if (page === 'agents')
705
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
706
  if (page === 'insights') return <Insights data={data} />;
707
  if (page === 'connect')
708
    return (
709
      <Connect data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
710
    );
711
  if (page === 'settings')
712
    return <SettingsPage settings={settings} setSettings={setSettings} api={api} />;
713
  return <Overview data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
714
}
715
716
function Overview({
717
  data,
718
  navigate,
719
  refreshDashboard,
720
  api,
721
}: {
722
  data: DashboardData;
723
  navigate: (page: Page) => void;
724
  refreshDashboard: () => Promise<void>;
725
  api: (path: string, init?: RequestInit) => Promise<unknown>;
726
}) {
727
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
728
  const warnings = data.events.filter((item) =>
729
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
730
  ).length;
731
  const publishedIds = new Set(
732
    data.publications
733
      .filter((publication) => publication.published)
734
      .map((publication) => String(publication.agentId)),
735
  );
736
  const runtimeIds = new Set(
737
    data.runtimes
738
      .filter((runtime) => runtime.status === 'verified')
739
      .map((runtime) => String(runtime.agentId)),
740
  );
741
  const modeOf = (agent: Record<string, any>) =>
742
    agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
743
  const readyAgents = data.agents.filter(
744
    (agent) =>
745
      publishedIds.has(String(agent.agentId)) &&
746
      (modeOf(agent) === 'temporary_chat' || runtimeIds.has(String(agent.agentId))),
747
  ).length;
748
  const activeTemporaryChats = data.agents.filter(
749
    (agent) => modeOf(agent) === 'temporary_chat' && agent.presence?.status === 'online',
750
  ).length;
751
  const pendingInvitations = data.federatedInteractions.filter(
752
    (interaction) => interaction.status === 'pending',
753
  ).length;
754
  const pendingFeedback = data.feedbackRequests.filter(
755
    (request) => request.status === 'pending',
756
  ).length;
757
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
758
  const reviewCount = warnings + pendingInvitations + pendingFeedback;
759
  return (
760
    <>
761
      <PageHead page="dashboard" action="Connect agent" onAction={() => navigate('connect')} />
762
      {pendingSetup > 0 && (
763
        <button className="attentionBanner" type="button" onClick={() => navigate('connect')}>
764
          <div>
765
            <span className="statusIndex">//</span>
766
            <div>
767
              <strong>
768
                {pendingSetup} setup request{pendingSetup === 1 ? '' : 's'} waiting
769
              </strong>
770
            </div>
771
          </div>
772
        </button>
773
      )}
774
      {!(readyAgents === data.agents.length && readyAgents) && (
775
        <section className="readiness">
776
          <div>
777
            <span className="statusIndex">!</span>
778
            <div>
779
              <strong>
780
                {readyAgents} of {data.agents.length} agents can receive A2A work
781
              </strong>
782
            </div>
783
          </div>
784
          <button className="secondary" type="button" onClick={() => navigate('agents')}>
785
            Manage
786
          </button>
787
        </section>
788
      )}
789
      <section className="metrics">
790
        <Metric
791
          label="Active temporary chats"
792
          value={activeTemporaryChats}
793
          note={`${runtimeIds.size} persistent runtimes connected`}
794
        />
795
        <Metric
796
          label="Interactions"
797
          value={data.interactions.length + data.federatedInteractions.length}
798
          note="local and shared"
799
        />
800
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
801
        <Metric
802
          label="Needs review"
803
          value={reviewCount}
804
          note={`${pendingFeedback} feedback · ${pendingInvitations} invitations`}
805
          warn={reviewCount > 0}
806
        />
807
      </section>
808
      <Invitations data={data} refreshDashboard={refreshDashboard} api={api} />
809
      <section className="contentGrid">
810
        <Panel title="Recent activity" subtitle="">
811
          <Timeline events={data.events.slice(-6).reverse()} />
812
          <TextButton onClick={() => navigate('history')}>All history</TextButton>
813
        </Panel>
814
        <Panel title="Reliability" subtitle="">
815
          {data.profiles.length ? (
816
            data.profiles.slice(0, 4).map((profile) => {
817
              const deltas = data.profileDeltas.filter(
818
                (delta) =>
819
                  delta.agentId === profile.agentId &&
820
                  delta.agentVersion === profile.agentVersion &&
821
                  delta.taskCategory === profile.taskCategory,
822
              );
823
              return (
824
                <Profile
825
                  key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
826
                  profile={profile}
827
                  deltas={deltas}
828
                />
829
              );
830
            })
831
          ) : (
832
            <Empty
833
              title="No reliability profile yet"
834
              text="Profiles appear after signed receipts and eligible bilateral feedback."
835
            />
836
          )}
837
        </Panel>
838
      </section>
839
    </>
840
  );
841
}
842
843
function Conversations({
844
  data,
845
  refreshDashboard,
846
  api,
847
}: {
848
  data: DashboardData;
849
  refreshDashboard: () => Promise<void>;
850
  api: (path: string, init?: RequestInit) => Promise<unknown>;
851
}) {
852
  const [selected, setSelected] = useState<Record<string, any> | null>(null);
853
  const [selectedAgentId, setSelectedAgentId] = useState('');
854
  const [draft, setDraft] = useState('');
855
  const [working, setWorking] = useState(false);
856
  const [error, setError] = useState('');
857
  const temporaryAgentFor = (thread: Record<string, any>) =>
858
    data.agents.find(
859
      (agent) =>
860
        thread.participantAgentIds?.includes(agent.agentId) &&
861
        (agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat')) ===
862
          'temporary_chat',
863
    );
864
  const openThread = async (thread: Record<string, any>) => {
865
    const agent = temporaryAgentFor(thread);
866
    if (!agent) return;
867
    setWorking(true);
868
    setError('');
869
    try {
870
      const [detail] = (await Promise.all([
871
        api(
872
          `/v0.1/agents/${encodeURIComponent(agent.agentId)}/threads/${encodeURIComponent(thread.threadId)}`,
873
        ),
874
        api(
875
          `/v0.1/agents/${encodeURIComponent(agent.agentId)}/threads/${encodeURIComponent(thread.threadId)}/read`,
876
          { method: 'POST' },
877
        ),
878
      ])) as [Record<string, any>, unknown];
879
      setSelected(detail);
880
      setSelectedAgentId(String(agent.agentId));
881
      await refreshDashboard();
882
    } catch (reason) {
883
      setError(reason instanceof Error ? reason.message : 'Could not open conversation');
884
    } finally {
885
      setWorking(false);
886
    }
887
  };
888
  const reply = async () => {
889
    if (!selected || !draft.trim() || !selectedAgentId) return;
890
    setWorking(true);
891
    setError('');
892
    try {
893
      await api(`/v0.1/agents/${encodeURIComponent(selectedAgentId)}/messages`, {
894
        method: 'POST',
895
        body: JSON.stringify({
896
          interactionId: selected.thread.interactionId,
897
          content: draft.trim(),
898
        }),
899
      });
900
      setDraft('');
901
      await openThread(selected.thread);
902
    } catch (reason) {
903
      setError(reason instanceof Error ? reason.message : 'Could not send reply');
904
      setWorking(false);
905
    }
906
  };
907
  return (
908
    <>
909
      <PageHead page="conversations" />
910
      <div className="notice">
911
        <strong>Temporary only.</strong> Encrypted 30 days. Persistent runtime messages stay direct.
912
      </div>
913
      {error ? (
914
        <div className="errorBar" role="alert">
915
          {error}
916
        </div>
917
      ) : null}
918
      <section className="conversationLayout">
919
        <Panel title="Threads" subtitle="Temporary chat identities only">
920
          <div className="threadList">
921
            {data.hostedThreads.length ? (
922
              data.hostedThreads.map((thread) => {
923
                const agent = temporaryAgentFor(thread);
924
                const peer = thread.participantAgentIds?.find(
925
                  (value: string) => value !== agent?.agentId,
926
                );
927
                return (
928
                  <button
929
                    type="button"
930
                    key={thread.threadId}
931
                    className={selected?.thread?.threadId === thread.threadId ? 'active' : ''}
932
                    onClick={() => void openThread(thread)}
933
                    disabled={working}
934
                  >
935
                    <span>
936
                      <strong>{peer ?? 'Counterparty'}</strong>
937
                      <small>{agent?.name ?? agent?.agentId}</small>
938
                    </span>
939
                    <b>{thread.unreadCount ? `${thread.unreadCount} NEW` : thread.status}</b>
940
                  </button>
941
                );
942
              })
943
            ) : (
944
              <Empty
945
                title="No temporary conversations"
946
                text="Messages appear after a temporary chat identity accepts an interaction with a persistent runtime."
947
              />
948
            )}
949
          </div>
950
        </Panel>
951
        <Panel
952
          title={selected ? 'Conversation' : 'Select a thread'}
953
          subtitle="Private insights are attributable and task-specific"
954
        >
955
          {selected ? (
956
            <div className="conversationDetail">
957
              {!!selected.insights?.length && (
958
                <div className="conversationInsights">
959
                  {selected.insights.map((insight: Record<string, any>) => (
960
                    <p key={insight.code}>
961
                      <b>{String(insight.severity).toUpperCase()}</b> {insight.message}
962
                    </p>
963
                  ))}
964
                </div>
965
              )}
966
              <div className="messageList">
967
                {selected.messages.map((message: Record<string, any>) => (
968
                  <article
969
                    key={message.messageId}
970
                    className={message.senderAgentId === selectedAgentId ? 'sent' : 'received'}
971
                  >
972
                    <small>{message.senderAgentId === selectedAgentId ? 'You' : 'Peer'}</small>
973
                    <p>{message.content}</p>
974
                    <span>{new Date(message.createdAt).toLocaleString()}</span>
975
                  </article>
976
                ))}
977
              </div>
978
              <div className="replyBox">
979
                <textarea
980
                  aria-label="Temporary chat reply"
981
                  value={draft}
982
                  onChange={(event) => setDraft(event.target.value)}
983
                  maxLength={20_000}
984
                  placeholder="Reply through the temporary OpenClasp A2A adapter"
985
                />
986
                <button
987
                  className="primary"
988
                  type="button"
989
                  disabled={working || !draft.trim() || selected.thread.status !== 'open'}
990
                  onClick={() => void reply()}
991
                >
992
                  {working ? 'Sending…' : 'Send reply'}
993
                </button>
994
              </div>
995
            </div>
996
          ) : (
997
            <Empty title="Nothing selected" text="Choose a thread to read its hosted history." />
998
          )}
999
        </Panel>
1000
      </section>
1001
    </>
1002
  );
1003
}
1004
1005
function History({ data }: { data: DashboardData }) {
1006
  const [query, setQuery] = useState('');
1007
  const [status, setStatus] = useState<
1008
    'all' | 'pending' | 'active' | 'provisional' | 'finalizing' | 'completed'
1009
  >('all');
1010
  const journeys = useMemo(() => interactionJourneys(data), [data]);
1011
  const filtered = useMemo(() => {
1012
    const needle = query.trim().toLowerCase();
1013
    return journeys.filter((journey) => {
1014
      if (status !== 'all' && journey.status !== status) return false;
1015
      if (!needle) return true;
1016
      const hay = [
1017
        journey.interactionId,
1018
        journey.purpose,
1019
        journey.taskCategory,
1020
        journey.status,
1021
        ...journey.participants,
1022
      ]
1023
        .filter(Boolean)
1024
        .join(' ')
1025
        .toLowerCase();
1026
      return hay.includes(needle);
1027
    });
1028
  }, [journeys, query, status]);
1029
  const [selectedId, setSelectedId] = useState('');
1030
  const selected = filtered.find((journey) => journey.interactionId === selectedId) ?? filtered[0];
1031
  return (
1032
    <>
1033
      <PageHead page="history" />
1034
      <div className="historyToolbar">
1035
        <Input
1036
          value={query}
1037
          onChange={(event) => setQuery(event.target.value)}
1038
          placeholder="Search purpose, category, or agent"
1039
          aria-label="Search interaction history"
1040
          className="h-9 min-w-[220px] flex-1 rounded-none border-0 border-b bg-transparent px-0 shadow-none dark:bg-transparent"
1041
        />
1042
        <Tabs
1043
          value={status === 'finalizing' ? 'all' : status}
1044
          onValueChange={(value) =>
1045
            setStatus(value as 'all' | 'pending' | 'active' | 'provisional' | 'completed')
1046
          }
1047
        >
1048
          <TabsList variant="line" aria-label="Interaction status filters">
1049
            {(['all', 'pending', 'active', 'provisional', 'completed'] as const).map((value) => (
1050
              <TabsTrigger key={value} value={value} className="capitalize">
1051
                {value}
1052
                <span className="font-mono text-[10px] text-muted-foreground">
1053
                  {value === 'all'
1054
                    ? journeys.length
1055
                    : journeys.filter((item) => item.status === value).length}
1056
                </span>
1057
              </TabsTrigger>
1058
            ))}
1059
          </TabsList>
1060
        </Tabs>
1061
      </div>
1062
      {filtered.length ? (
1063
        <section className="journeyLayout">
1064
          <div className="journeyList" aria-label="Interactions">
1065
            {filtered.map((journey) => (
1066
              <button
1067
                key={journey.interactionId}
1068
                className={selected?.interactionId === journey.interactionId ? 'active' : ''}
1069
                type="button"
1070
                onClick={() => {
1071
                  setSelectedId(journey.interactionId);
1072
                  if (matchMedia('(max-width: 980px)').matches)
1073
                    requestAnimationFrame(() =>
1074
                      document
1075
                        .querySelector('.journeyDetail')
1076
                        ?.scrollIntoView({ behavior: 'smooth' }),
1077
                    );
1078
                }}
1079
              >
1080
                <span className="journeyListTop">
1081
                  <b>{journey.taskCategory}</b>
1082
                  <StatusPill value={journey.status} />
1083
                </span>
1084
                <strong>{journey.purpose}</strong>
1085
                <small>{journey.participants.join(' ↔ ') || 'Participants unavailable'}</small>
1086
                <span className="journeyListMeta">
1087
                  {relativeTime(journey.updatedAt)}
1088
                  <i>{journey.stepCount} recorded steps</i>
1089
                </span>
1090
              </button>
1091
            ))}
1092
          </div>
1093
          {selected ? <InteractionJourney journey={selected} /> : null}
1094
        </section>
1095
      ) : (
1096
        <section className="panel">
1097
          <Empty
1098
            title={journeys.length ? 'No matching interactions' : 'No interactions recorded'}
1099
            text={
1100
              journeys.length
1101
                ? 'Clear the search or choose another status.'
1102
                : 'Completed contracts, outcomes, feedback, and learning will appear as one readable journey.'
1103
            }
1104
          />
1105
        </section>
1106
      )}
1107
    </>
1108
  );
1109
}
1110
1111
type InteractionJourneyModel = {
1112
  interactionId: string;
1113
  purpose: string;
1114
  taskCategory: string;
1115
  status: string;
1116
  participants: string[];
1117
  updatedAt: string;
1118
  stepCount: number;
1119
  interaction: Record<string, any>;
1120
  session?: Record<string, any>;
1121
  events: Record<string, any>[];
1122
  briefs: Record<string, any>[];
1123
  reports: Record<string, any>[];
1124
  feedbackRequests: Record<string, any>[];
1125
  feedback: Record<string, any>[];
1126
  conclusion?: Record<string, any>;
1127
  receipt?: Record<string, any>;
1128
  eligibility?: Record<string, any>;
1129
  deltas: Record<string, any>[];
1130
  conflicts: Record<string, any>[];
1131
};
1132
1133
function interactionJourneys(data: DashboardData): InteractionJourneyModel[] {
1134
  const ids = new Set<string>();
1135
  const collections = [
1136
    data.interactions,
1137
    data.federatedInteractions,
1138
    data.liveSessions,
1139
    data.events,
1140
    data.counterpartyBriefs,
1141
    data.completionReports,
1142
    data.feedbackRequests,
1143
    data.interactionFeedback,
1144
    data.interactionConclusions,
1145
    data.receipts,
1146
    data.learningEligibility,
1147
    data.profileDeltas,
1148
    data.conflicts,
1149
  ];
1150
  for (const collection of collections)
1151
    for (const item of collection) if (item.interactionId) ids.add(String(item.interactionId));
1152
  return [...ids]
1153
    .map((interactionId) => {
1154
      const interaction = data.federatedInteractions.find(
1155
        (item) => item.interactionId === interactionId,
1156
      ) ??
1157
        data.interactions.find((item) => item.interactionId === interactionId) ?? { interactionId };
1158
      const session = data.liveSessions.find((item) => item.interactionId === interactionId);
1159
      const events = data.events.filter((item) => item.interactionId === interactionId);
1160
      const briefs = data.counterpartyBriefs.filter((item) => item.interactionId === interactionId);
1161
      const reports = data.completionReports.filter((item) => item.interactionId === interactionId);
1162
      const feedbackRequests = data.feedbackRequests.filter(
1163
        (item) => item.interactionId === interactionId,
1164
      );
1165
      const feedback = data.interactionFeedback.filter(
1166
        (item) => item.interactionId === interactionId,
1167
      );
1168
      const conclusion = data.interactionConclusions.find(
1169
        (item) => item.interactionId === interactionId,
1170
      );
1171
      const receipt = data.receipts.find((item) => item.interactionId === interactionId);
1172
      const eligibility = data.learningEligibility.find(
1173
        (item) => item.interactionId === interactionId,
1174
      );
1175
      const deltas = data.profileDeltas.filter((item) => item.interactionId === interactionId);
1176
      const conflicts = data.conflicts.filter((item) => item.interactionId === interactionId);
1177
      const participants = [
1178
        interaction.initiatorAgentId,
1179
        interaction.responderAgentId,
1180
        interaction.agentId,
1181
        ...reports.flatMap((report) => [report.reportingAgentId, report.counterpartyAgentId]),
1182
      ]
1183
        .filter((value): value is string => typeof value === 'string' && Boolean(value))
1184
        .filter((value, index, values) => values.indexOf(value) === index);
1185
      const dates = [
1186
        interaction.updatedAt,
1187
        interaction.createdAt,
1188
        session?.completedAt,
1189
        session?.activatedAt,
1190
        ...events.map(timestamp),
1191
        ...reports.map(timestamp),
1192
        ...feedback.map(timestamp),
1193
        conclusion?.generatedAt,
1194
        receipt?.completedAt,
1195
        eligibility?.decidedAt,
1196
      ].filter((value): value is string => typeof value === 'string');
1197
      return {
1198
        interactionId,
1199
        purpose: String(
1200
          interaction.contract?.purpose ??
1201
            interaction.contract?.requestedOutcome ??
1202
            conclusion?.summary ??
1203
            'Assured agent interaction',
1204
        ),
1205
        taskCategory: String(interaction.contract?.taskCategory ?? 'general'),
1206
        status: String(
1207
          conclusion || receipt
1208
            ? conclusion?.lifecycle === 'provisional' || receipt?.provisional
1209
              ? 'provisional'
1210
              : 'completed'
1211
            : reports.length > 0
1212
              ? 'finalizing'
1213
              : (session?.status ?? interaction.status ?? 'pending'),
1214
        ),
1215
        participants,
1216
        updatedAt:
1217
          dates.sort((left, right) => Date.parse(right) - Date.parse(left))[0] ??
1218
          new Date(0).toISOString(),
1219
        stepCount:
1220
          1 +
1221
          Math.max(0, Number(interaction.contractRevisions?.length ?? 0) - 1) +
1222
          events.length +
1223
          briefs.length +
1224
          reports.length +
1225
          feedbackRequests.length +
1226
          feedback.length +
1227
          conflicts.length +
1228
          Number(Boolean(conclusion)) +
1229
          Number(Boolean(receipt)) +
1230
          Number(Boolean(eligibility)) +
1231
          deltas.length,
1232
        interaction,
1233
        ...(session ? { session } : {}),
1234
        events,
1235
        briefs,
1236
        reports,
1237
        feedbackRequests,
1238
        feedback,
1239
        ...(conclusion ? { conclusion } : {}),
1240
        ...(receipt ? { receipt } : {}),
1241
        ...(eligibility ? { eligibility } : {}),
1242
        deltas,
1243
        conflicts,
1244
      };
1245
    })
1246
    .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
1247
}
1248
1249
function InteractionJourney({ journey }: { journey: InteractionJourneyModel }) {
1250
  const timeline = interactionTimeline(journey);
1251
  const conclusion = journey.conclusion;
1252
  const checkpoints = journey.events.filter(
1253
    (event) => (event.type ?? event.eventType) === 'progress_checkpoint' && event.checkpoint,
1254
  );
1255
  const latestCheckpoint = checkpoints.at(-1)?.checkpoint;
1256
  const progress = Number(latestCheckpoint?.progress ?? 0);
1257
  const submittedFeedback = new Set(
1258
    journey.feedback.map((item) => item.reviewerAgentId).filter(Boolean),
1259
  ).size;
1260
  return (
1261
    <article className="journeyDetail">
1262
      <header className="journeyHero">
1263
        <div>
1264
          <span className="eyebrow">{journey.taskCategory}</span>
1265
          <h2>{journey.purpose}</h2>
1266
          <p>{journey.participants.join(' ↔ ')}</p>
1267
        </div>
1268
        <StatusPill value={conclusion?.outcome ?? journey.status} />
1269
      </header>
1270
1271
      <div className="journeySummaryGrid">
1272
        <section>
1273
          <span>Progress</span>
1274
          <strong>
1275
            {latestCheckpoint
1276
              ? `${Math.round(progress * 100)}% · ${humanize(latestCheckpoint.state)}`
1277
              : journey.reports.length
1278
                ? 'One side ended'
1279
                : 'Awaiting checkpoint'}
1280
          </strong>
1281
          <small>
1282
            {latestCheckpoint
1283
              ? `${humanize(latestCheckpoint.topicStatus)} · ${Math.round(Number(latestCheckpoint.confidence ?? 0) * 100)}% confidence`
1284
              : 'Agents report compact progress every few exchanges'}
1285
          </small>
1286
        </section>
1287
        <section>
1288
          <span>Outcome</span>
1289
          <strong>
1290
            {conclusion
1291
              ? humanize(conclusion.outcome)
1292
              : `${journey.reports.length}/2 terminal reports`}
1293
          </strong>
1294
          <small>
1295
            {conclusion
1296
              ? conclusion.lifecycle === 'provisional'
1297
                ? `${Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer ${humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()}`
1298
                : humanize(conclusion.consensus)
1299
              : journey.reports.length === 1
1300
                ? 'One agent finished; confirming with the peer'
1301
                : 'Conversation remains active'}
1302
          </small>
1303
        </section>
1304
        <section>
1305
          <span>Sealed feedback</span>
1306
          <strong>
1307
            {journey.feedbackRequests.some((request) => request.status === 'pending')
1308
              ? `${submittedFeedback}/2 sealed`
1309
              : 'Window closed'}
1310
          </strong>
1311
          <small>{feedbackState(journey.feedbackRequests)}</small>
1312
        </section>
1313
        <section>
1314
          <span>Learning</span>
1315
          <strong>
1316
            {journey.eligibility
1317
              ? journey.eligibility.eligible
1318
                ? `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% weight`
1319
                : 'Not eligible'
1320
              : conclusion?.lifecycle === 'provisional'
1321
                ? 'Pending corroboration'
1322
                : 'Not assessed'}
1323
          </strong>
1324
          <small>{humanize(journey.eligibility?.contributionMode ?? 'Local only')}</small>
1325
        </section>
1326
      </div>
1327
1328
      {latestCheckpoint ? (
1329
        <section className="progressCard">
1330
          <div className="progressCardTop">
1331
            <div>
1332
              <span className="eyebrow">LATEST PRIVATE CHECKPOINT</span>
1333
              <h3>{humanize(latestCheckpoint.state)}</h3>
1334
            </div>
1335
            <strong>{Math.round(progress * 100)}%</strong>
1336
          </div>
1337
          <div
1338
            className="progressTrack"
1339
            role="progressbar"
1340
            aria-label="Interaction progress"
1341
            aria-valuemin={0}
1342
            aria-valuemax={100}
1343
            aria-valuenow={Math.round(progress * 100)}
1344
          >
1345
            <i style={{ width: `${Math.round(progress * 100)}%` }} />
1346
          </div>
1347
          <div className="progressFacts">
1348
            <span>{latestCheckpoint.criteriaMet?.length ?? 0} criteria met</span>
1349
            <span>{latestCheckpoint.criteriaRemaining?.length ?? 0} remaining</span>
1350
            <span>{latestCheckpoint.expectedRemainingTurns ?? '—'} expected turns</span>
1351
            <span>
1352
              {latestCheckpoint.needsHuman ? 'Human input needed' : 'No human input needed'}
1353
            </span>
1354
          </div>
1355
          {latestCheckpoint.topicStatus !== 'in_scope' ? (
1356
            <p className="progressWarning">
1357
              Topic {humanize(latestCheckpoint.topicStatus)}. Amend the contract or start a new
1358
              interaction before continuing.
1359
            </p>
1360
          ) : null}
1361
        </section>
1362
      ) : null}
1363
1364
      {conclusion ? (
1365
        <section className="outcomeCard">
1366
          <div>
1367
            <span className="eyebrow">
1368
              {conclusion.lifecycle === 'provisional' ? 'PROVISIONAL INSIGHT' : 'WHAT HAPPENED'}
1369
            </span>
1370
            <h3>{conclusion.summary}</h3>
1371
            {conclusion.lifecycle === 'provisional' ? (
1372
              <p className="progressWarning">
1373
                Based on {conclusion.reportIds?.length ?? 1} participant report. Missing:{' '}
1374
                {(conclusion.missingReportAgentIds ?? []).join(', ') || 'peer report'}. This is
1375
                usable now at reduced confidence and will be revised if the peer responds.
1376
              </p>
1377
            ) : null}
1378
          </div>
1379
          {!!conclusion.criteria?.length && (
1380
            <ul>
1381
              {conclusion.criteria.map((criterion: Record<string, any>) => (
1382
                <li key={criterion.criterion}>
1383
                  <StatusPill value={String(criterion.status)} />
1384
                  <span>{criterion.criterion}</span>
1385
                </li>
1386
              ))}
1387
            </ul>
1388
          )}
1389
          {!!Object.keys(conclusion.averageRatings ?? {}).length && (
1390
            <div className="ratingGrid">
1391
              {Object.entries(conclusion.averageRatings).map(([label, value]) => (
1392
                <Meter key={label} label={humanize(label)} value={Number(value)} />
1393
              ))}
1394
            </div>
1395
          )}
1396
        </section>
1397
      ) : (
1398
        <div className="journeyNotice">
1399
          {journey.reports.length === 1
1400
            ? 'Finalizing: one agent submitted its terminal report. OpenClasp has requested independent confirmation from the peer.'
1401
            : journey.reports.length >= 2
1402
              ? 'Both agents finished. Sealed feedback remains private until both respond or the feedback window expires.'
1403
              : 'Active interaction. Progress checkpoints appear after several meaningful exchanges; either agent can finalize immediately when the task is done.'}
1404
        </div>
1405
      )}
1406
1407
      <section className="journeyTimelineSection">
1408
        <div>
1409
          <h3>Interaction timeline</h3>
1410
          <small>Contract → session → outcome → feedback → learning</small>
1411
        </div>
1412
        <ol className="journeyTimeline">
1413
          {timeline.map((step, index) => (
1414
            <li key={`${step.at}-${step.title}-${index}`}>
1415
              <span className={`journeyNode ${step.tone}`} />
1416
              <div>
1417
                <span>{formatDate(step.at)}</span>
1418
                <strong>{step.title}</strong>
1419
                <p>{step.detail}</p>
1420
              </div>
1421
              <StatusPill value={step.status} />
1422
            </li>
1423
          ))}
1424
        </ol>
1425
      </section>
1426
    </article>
1427
  );
1428
}
1429
1430
function interactionTimeline(journey: InteractionJourneyModel) {
1431
  const revisions = journey.interaction.contractRevisions ?? [];
1432
  const contractSteps = revisions.length
1433
    ? revisions.map((revision: Record<string, any>) => ({
1434
        at: String(revision.updatedAt ?? revision.createdAt ?? journey.interaction.createdAt),
1435
        title: `Contract v${revision.revision} ${humanize(revision.status)}`,
1436
        detail: `${revision.proposedByAgentId} · ${Object.keys(revision.acceptances ?? {}).length}/${journey.participants.length || 2} accepted · ${String(revision.termsHash).slice(0, 12)}…`,
1437
        status: String(revision.status),
1438
        tone:
1439
          revision.status === 'accepted'
1440
            ? 'good'
1441
            : revision.status === 'rejected'
1442
              ? 'danger'
1443
              : revision.status === 'proposed'
1444
                ? 'warn'
1445
                : 'neutral',
1446
      }))
1447
    : [
1448
        {
1449
          at: String(journey.interaction.createdAt ?? journey.updatedAt),
1450
          title: 'Contract proposed',
1451
          detail: `${journey.participants.length || 2} participants · ${journey.taskCategory}`,
1452
          status: String(journey.interaction.status ?? 'recorded'),
1453
          tone: 'neutral',
1454
        },
1455
        ...Object.values(journey.interaction.acceptances ?? {}).map((acceptance: any) => ({
1456
          at: String(acceptance.acceptedAt ?? journey.interaction.updatedAt ?? journey.updatedAt),
1457
          title: `Contract accepted by ${acceptance.agentId}`,
1458
          detail: `Acceptance method: ${humanize(acceptance.method ?? 'recorded')}`,
1459
          status: 'accepted',
1460
          tone: 'good',
1461
        })),
1462
      ];
1463
  const steps: { at: string; title: string; detail: string; status: string; tone: string }[] = [
1464
    ...contractSteps,
1465
    ...journey.briefs.map((brief) => ({
1466
      at: String(brief.generatedAt ?? journey.updatedAt),
1467
      title: `Private counterparty brief for ${brief.recipientAgentId}`,
1468
      detail: `${brief.relevantSampleSize ?? 0} relevant samples · ${Math.round(Number(brief.historyConfidence ?? 0) * 100)}% history confidence`,
1469
      status: String(brief.decision ?? 'ready'),
1470
      tone: brief.decision === 'DENY' ? 'danger' : brief.decision === 'CHALLENGE' ? 'warn' : 'good',
1471
    })),
1472
    ...(journey.session?.activatedAt
1473
      ? [
1474
          {
1475
            at: String(journey.session.activatedAt),
1476
            title: 'A2A session activated',
1477
            detail:
1478
              'Scoped credentials were issued. Persistent runtimes exchange content directly; temporary mode uses the hosted adapter.',
1479
            status: 'active',
1480
            tone: 'good',
1481
          },
1482
        ]
1483
      : []),
1484
    ...journey.events.map((event) => {
1485
      const eventType = String(event.type ?? event.eventType ?? 'structured_event');
1486
      const checkpoint = event.checkpoint;
1487
      return {
1488
        at: timestamp(event),
1489
        title:
1490
          eventType === 'progress_checkpoint'
1491
            ? `${Math.round(Number(checkpoint?.progress ?? 0) * 100)}% progress checkpoint`
1492
            : humanize(eventType),
1493
        detail:
1494
          eventType === 'progress_checkpoint'
1495
            ? `${event.agentId} · ${humanize(checkpoint?.state ?? 'active')} · ${humanize(checkpoint?.topicStatus ?? 'in_scope')}`
1496
            : String(event.agentId ?? 'OpenClasp') + ' submitted structured metadata.',
1497
        status: String(checkpoint?.state ?? event.visibility ?? 'recorded'),
1498
        tone:
1499
          checkpoint?.topicStatus === 'changed' || checkpoint?.state === 'blocked'
1500
            ? 'warn'
1501
            : eventType.includes('violation')
1502
              ? 'danger'
1503
              : eventType.includes('warning')
1504
                ? 'warn'
1505
                : checkpoint
1506
                  ? 'good'
1507
                  : 'neutral',
1508
      };
1509
    }),
1510
    ...journey.reports.map((report) => ({
1511
      at: timestamp(report),
1512
      title: `Completion reported by ${report.reportingAgentId}`,
1513
      detail: String(report.summary ?? 'Structured completion report submitted.'),
1514
      status: String(report.outcome ?? 'reported'),
1515
      tone:
1516
        report.outcome === 'failure' ? 'danger' : report.outcome === 'partial' ? 'warn' : 'good',
1517
    })),
1518
    ...journey.feedback.map((item) => ({
1519
      at: timestamp(item),
1520
      title: `Feedback submitted by ${item.reviewerAgentId}`,
1521
      detail: `Would work again: ${humanize(item.wouldWorkAgain ?? 'unsure')} · private comment concealed`,
1522
      status: 'sealed',
1523
      tone: 'neutral',
1524
    })),
1525
    ...journey.feedbackRequests
1526
      .filter((request) => request.status !== 'submitted')
1527
      .map((request) => ({
1528
        at: String(request.requestedAt ?? journey.updatedAt),
1529
        title: `Feedback request ${humanize(request.status)}`,
1530
        detail:
1531
          request.status === 'pending'
1532
            ? `Response due ${formatDate(String(request.dueAt))}`
1533
            : 'The bilateral conclusion continued without this response.',
1534
        status: String(request.status),
1535
        tone: request.status === 'pending' ? 'warn' : 'neutral',
1536
      })),
1537
    ...journey.conflicts.map((conflict) => ({
1538
      at: timestamp(conflict),
1539
      title: 'Dispute recorded',
1540
      detail: String(conflict.summary ?? 'The interaction has a structured dispute record.'),
1541
      status: String(conflict.status ?? 'open'),
1542
      tone: conflict.status === 'resolved' ? 'good' : 'danger',
1543
    })),
1544
    ...(journey.conclusion
1545
      ? [
1546
          {
1547
            at: String(journey.conclusion.generatedAt),
1548
            title:
1549
              journey.conclusion.lifecycle === 'provisional'
1550
                ? 'Provisional insight generated'
1551
                : 'Conclusion released',
1552
            detail: String(journey.conclusion.summary),
1553
            status: String(journey.conclusion.consensus),
1554
            tone:
1555
              journey.conclusion.lifecycle === 'provisional' ||
1556
              journey.conclusion.consensus === 'conflicting'
1557
                ? 'warn'
1558
                : 'good',
1559
          },
1560
        ]
1561
      : []),
1562
    ...(journey.receipt
1563
      ? [
1564
          {
1565
            at: timestamp(journey.receipt),
1566
            title: journey.receipt.provisional
1567
              ? 'Provisional outcome attested'
1568
              : 'Outcome receipt attested',
1569
            detail: journey.receipt.provisional
1570
              ? 'The available one-sided report was sealed without claiming peer agreement.'
1571
              : 'Contract result, commitment status, and evidence hashes were sealed.',
1572
            status: String(journey.receipt.outcome ?? 'signed'),
1573
            tone: journey.receipt.outcome === 'failure' ? 'danger' : 'good',
1574
          },
1575
        ]
1576
      : []),
1577
    ...(journey.eligibility
1578
      ? [
1579
          {
1580
            at: String(journey.eligibility.decidedAt),
1581
            title: journey.eligibility.eligible
1582
              ? 'Eligible history applied'
1583
              : 'History excluded from profile',
1584
            detail: `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% sample weight · ${humanize(journey.eligibility.contributionMode)}`,
1585
            status: journey.eligibility.eligible ? 'attested' : 'excluded',
1586
            tone: journey.eligibility.eligible ? 'good' : 'warn',
1587
          },
1588
        ]
1589
      : []),
1590
  ];
1591
  return steps.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
1592
}
1593
1594
function Agents({
1595
  data,
1596
  navigate,
1597
  refreshDashboard,
1598
  api,
1599
}: {
1600
  data: DashboardData;
1601
  navigate: (page: Page) => void;
1602
  refreshDashboard: () => Promise<void>;
1603
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1604
}) {
1605
  const [working, setWorking] = useState('');
1606
  const [error, setError] = useState('');
1607
  const saveAutomation = async (
1608
    agentId: string,
1609
    value: {
1610
      autoPublish: boolean;
1611
      autoAcceptPolicy: 'off' | 'safe_matching';
1612
      autoAcceptTaskCategories: string[];
1613
    },
1614
  ) => {
1615
    setWorking(agentId);
1616
    setError('');
1617
    try {
1618
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/automation`, {
1619
        method: 'PUT',
1620
        body: JSON.stringify(value),
1621
      });
1622
      await refreshDashboard();
1623
      return true;
1624
    } catch (reason) {
1625
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
1626
      return false;
1627
    } finally {
1628
      setWorking('');
1629
    }
1630
  };
1631
  const saveRuntime = async (agentId: string, endpoint: string) => {
1632
    setWorking(`runtime:${agentId}`);
1633
    setError('');
1634
    try {
1635
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, {
1636
        method: 'PUT',
1637
        body: JSON.stringify({ endpoint }),
1638
      });
1639
      await refreshDashboard();
1640
    } catch (reason) {
1641
      setError(reason instanceof Error ? reason.message : 'Runtime verification failed');
1642
    } finally {
1643
      setWorking('');
1644
    }
1645
  };
1646
  const disableRuntime = async (agentId: string) => {
1647
    setWorking(`runtime:${agentId}`);
1648
    setError('');
1649
    try {
1650
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, { method: 'DELETE' });
1651
      await refreshDashboard();
1652
    } catch (reason) {
1653
      setError(reason instanceof Error ? reason.message : 'Runtime update failed');
1654
    } finally {
1655
      setWorking('');
1656
    }
1657
  };
1658
  const revokeAccessToken = async (agentId: string, tokenId: string) => {
1659
    setWorking(`token:${agentId}`);
1660
    setError('');
1661
    try {
1662
      await api(
1663
        `/v0.1/agents/${encodeURIComponent(agentId)}/access-tokens/${encodeURIComponent(tokenId)}`,
1664
        { method: 'DELETE' },
1665
      );
1666
      await refreshDashboard();
1667
    } catch (reason) {
1668
      setError(reason instanceof Error ? reason.message : 'Could not revoke agent access token');
1669
    } finally {
1670
      setWorking('');
1671
    }
1672
  };
1673
  const deleteAgent = async (agent: Record<string, any>) => {
1674
    const agentId = String(agent.agentId);
1675
    if (
1676
      !window.confirm(
1677
        `Delete “${String(agent.name ?? agentId)}”?\n\nIts runtime, publication, presence, MCP binding and hosted temporary messages will be removed. Signed interaction and receipt history will be retained.`,
1678
      )
1679
    )
1680
      return;
1681
    setWorking(`delete:${agentId}`);
1682
    setError('');
1683
    try {
1684
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
1685
      await refreshDashboard();
1686
    } catch (reason) {
1687
      setError(reason instanceof Error ? reason.message : 'Agent deletion failed');
1688
    } finally {
1689
      setWorking('');
1690
    }
1691
  };
1692
  return (
1693
    <>
1694
      <PageHead page="agents" action="Connect agent" onAction={() => navigate('connect')} />
1695
      <div className="notice">
1696
        <strong>Two explicit modes.</strong> Persistent runtimes talk directly over A2A. Temporary
1697
        chats use an OpenClasp-hosted endpoint with encrypted-at-rest history. There is no offline
1698
        relay for persistent runtimes.
1699
      </div>
1700
      {error && (
1701
        <div className="errorBar" role="alert">
1702
          {error}
1703
        </div>
1704
      )}
1705
      <section className="agentGrid">
1706
        {data.agents.length ? (
1707
          data.agents.map((agent) => (
1708
            <AgentCard
1709
              key={agent.agentId}
1710
              agent={agent}
1711
              projectName={
1712
                data.projects.find((project) => project.projectId === agent.projectId)?.name
1713
              }
1714
              published={data.publications.some(
1715
                (publication) => publication.agentId === agent.agentId && publication.published,
1716
              )}
1717
              working={working === agent.agentId}
1718
              onSave={(value) => saveAutomation(agent.agentId, value)}
1719
              runtime={data.runtimes.find((runtime) => runtime.agentId === agent.agentId)}
1720
              runtimeWorking={working === `runtime:${agent.agentId}`}
1721
              deleteWorking={working === `delete:${agent.agentId}`}
1722
              onRuntime={(endpoint) => saveRuntime(agent.agentId, endpoint)}
1723
              onDisableRuntime={() => disableRuntime(agent.agentId)}
1724
              accessTokens={data.accessTokens.filter(
1725
                (token) =>
1726
                  token.agentId === agent.agentId &&
1727
                  !token.revokedAt &&
1728
                  Date.parse(String(token.expiresAt)) > Date.now(),
1729
              )}
1730
              accessTokenWorking={working === `token:${agent.agentId}`}
1731
              onRevokeAccessToken={(tokenId) => revokeAccessToken(agent.agentId, tokenId)}
1732
              onDelete={() => deleteAgent(agent)}
1733
            />
1734
          ))
1735
        ) : (
1736
          <Empty
1737
            title="No agents connected"
1738
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
1739
            action="Open connection guide"
1740
            onAction={() => navigate('connect')}
1741
          />
1742
        )}
1743
      </section>
1744
    </>
1745
  );
1746
}
1747
1748
function Invitations({
1749
  data,
1750
  refreshDashboard,
1751
  api,
1752
}: {
1753
  data: DashboardData;
1754
  refreshDashboard: () => Promise<void>;
1755
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1756
}) {
1757
  const [working, setWorking] = useState('');
1758
  const [error, setError] = useState('');
1759
  const ownedAgentIds = useMemo(
1760
    () => new Set(data.agents.map((agent) => String(agent.agentId))),
1761
    [data.agents],
1762
  );
1763
  const currentProposal = (interaction: Record<string, any>) =>
1764
    [...(interaction.contractRevisions ?? [])]
1765
      .reverse()
1766
      .find((revision) => revision.status === 'proposed');
1767
  const respondingAgent = (interaction: Record<string, any>) => {
1768
    const proposal = currentProposal(interaction);
1769
    return (interaction.contract?.parties ?? []).find(
1770
      (agentId: string) => ownedAgentIds.has(String(agentId)) && !proposal?.acceptances?.[agentId],
1771
    );
1772
  };
1773
  const incoming = data.federatedInteractions.filter(
1774
    (interaction) => currentProposal(interaction) && respondingAgent(interaction),
1775
  );
1776
  const respond = async (
1777
    interactionId: string,
1778
    revisionId: string,
1779
    agentId: string,
1780
    decision: 'accept' | 'reject',
1781
  ) => {
1782
    setWorking(interactionId);
1783
    setError('');
1784
    try {
1785
      await api(
1786
        `/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals/${encodeURIComponent(revisionId)}/respond`,
1787
        {
1788
          method: 'POST',
1789
          body: JSON.stringify({ agentId, decision }),
1790
        },
1791
      );
1792
      await refreshDashboard();
1793
    } catch (reason) {
1794
      setError(reason instanceof Error ? reason.message : 'Could not respond to invitation');
1795
    } finally {
1796
      setWorking('');
1797
    }
1798
  };
1799
  if (!incoming.length && !data.federatedInteractions.length) return null;
1800
  return (
1801
    <Panel
1802
      title={incoming.length ? `Contract decisions (${incoming.length})` : 'Shared interactions'}
1803
      subtitle=""
1804
    >
1805
      {error ? (
1806
        <div className="errorBar" role="alert">
1807
          {error}
1808
        </div>
1809
      ) : null}
1810
      {(incoming.length ? incoming : data.federatedInteractions.slice(0, 5)).map((interaction) => (
1811
        <article className="setupRequest" key={interaction.interactionId}>
1812
          <div>
1813
            <strong>{interaction.contract?.purpose ?? interaction.interactionId}</strong>
1814
            <small>
1815
              {interaction.initiatorAgentId} → {interaction.responderAgentId} · {interaction.status}
1816
            </small>
1817
            <small>
1818
              Contract v{interaction.contractRevision ?? 1}:{' '}
1819
              {String(currentProposal(interaction)?.termsHash ?? interaction.termsHash).slice(
1820
                0,
1821
                16,
1822
              )}
1823
              …
1824
              {currentProposal(interaction)
1825
                ? ` · proposed by ${currentProposal(interaction).proposedByAgentId}`
1826
                : ' · agreed'}
1827
            </small>
1828
            {interaction.contractRevisions?.length > 1 ? (
1829
              <small>{interaction.contractRevisions.length} recorded contract revisions</small>
1830
            ) : null}
1831
            {data.liveSessions.find(
1832
              (session) => session.interactionId === interaction.interactionId,
1833
            ) ? (
1834
              <small className="autoAccepted">
1835
                Live session:{' '}
1836
                {
1837
                  data.liveSessions.find(
1838
                    (session) => session.interactionId === interaction.interactionId,
1839
                  )?.status
1840
                }
1841
              </small>
1842
            ) : null}
1843
            {interaction.status === 'active' &&
1844
            interaction.acceptances?.[interaction.responderAgentId]?.method ===
1845
              'policy_auto_accept' ? (
1846
              <small className="autoAccepted">✓ Auto-approved by the responder's safe policy</small>
1847
            ) : null}
1848
          </div>
1849
          {currentProposal(interaction) && respondingAgent(interaction) ? (
1850
            <div className="decisionButtons">
1851
              <button
1852
                className="secondary"
1853
                type="button"
1854
                disabled={working === interaction.interactionId}
1855
                onClick={() =>
1856
                  void respond(
1857
                    interaction.interactionId,
1858
                    currentProposal(interaction).revisionId,
1859
                    respondingAgent(interaction),
1860
                    'reject',
1861
                  )
1862
                }
1863
              >
1864
                Reject
1865
              </button>
1866
              <button
1867
                className="primary"
1868
                type="button"
1869
                disabled={working === interaction.interactionId}
1870
                onClick={() =>
1871
                  void respond(
1872
                    interaction.interactionId,
1873
                    currentProposal(interaction).revisionId,
1874
                    respondingAgent(interaction),
1875
                    'accept',
1876
                  )
1877
                }
1878
              >
1879
                {working === interaction.interactionId
1880
                  ? 'Working…'
1881
                  : interaction.status === 'active'
1882
                    ? 'Accept amendment'
1883
                    : 'Accept contract'}
1884
              </button>
1885
            </div>
1886
          ) : null}
1887
        </article>
1888
      ))}
1889
    </Panel>
1890
  );
1891
}
1892
1893
function Insights({ data }: { data: DashboardData }) {
1894
  const eligible = data.learningEligibility.filter((item) => item.eligible);
1895
  const pendingFeedback = data.feedbackRequests.filter((item) => item.status === 'pending');
1896
  const provisional = data.interactionConclusions.filter(
1897
    (item) => item.lifecycle === 'provisional',
1898
  );
1899
  return (
1900
    <>
1901
      <PageHead page="insights" />
1902
      <section className="insightPrinciple">
1903
        <div>
1904
          <p className="pageKicker">
1905
            <span>//</span> how to read this
1906
          </p>
1907
          <h2>Reliability is contextual, not a leaderboard.</h2>
1908
          <p>
1909
            Every profile is tied to an agent version and task category. Scores reflect eligible
1910
            structured outcomes and decay as history gets older.
1911
          </p>
1912
        </div>
1913
        <div className="insightStats">
1914
          <span>
1915
            <strong>{data.profiles.length}</strong> contextual profiles
1916
          </span>
1917
          <span>
1918
            <strong>{eligible.length}</strong> eligible interactions
1919
          </span>
1920
          <span>
1921
            <strong>{pendingFeedback.length}</strong> feedback pending
1922
          </span>
1923
          <span>
1924
            <strong>{provisional.length}</strong> provisional signals
1925
          </span>
1926
        </div>
1927
      </section>
1928
      {!!provisional.length && (
1929
        <Panel
1930
          title="Provisional interaction signals"
1931
          subtitle="Available now, kept separate from corroborated reliability history"
1932
        >
1933
          <div className="learningDecisionList">
1934
            {provisional
1935
              .slice()
1936
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
1937
              .map((conclusion) => (
1938
                <article key={conclusion.conclusionId}>
1939
                  <StatusPill value="provisional" />
1940
                  <div>
1941
                    <strong>{conclusion.summary}</strong>
1942
                    <small>
1943
                      {Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer{' '}
1944
                      {humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()} ·{' '}
1945
                      {relativeTime(timestamp(conclusion))}
1946
                    </small>
1947
                    <p>
1948
                      Missing report from{' '}
1949
                      {(conclusion.missingReportAgentIds ?? []).join(', ') || 'the counterparty'}.
1950
                      The signal will be revised if more structured evidence arrives.
1951
                    </p>
1952
                  </div>
1953
                </article>
1954
              ))}
1955
          </div>
1956
        </Panel>
1957
      )}
1958
      <section className="insightGrid">
1959
        {data.profiles.length ? (
1960
          data.profiles.map((profile) => {
1961
            const deltas = data.profileDeltas
1962
              .filter(
1963
                (delta) =>
1964
                  delta.agentId === profile.agentId &&
1965
                  delta.agentVersion === profile.agentVersion &&
1966
                  delta.taskCategory === profile.taskCategory,
1967
              )
1968
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)));
1969
            return (
1970
              <Panel
1971
                key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
1972
                title={profile.agentId}
1973
                subtitle={`${humanize(profile.taskCategory)} · version ${profile.agentVersion}`}
1974
              >
1975
                <Profile profile={profile} expanded deltas={deltas} showHeader={false} />
1976
              </Panel>
1977
            );
1978
          })
1979
        ) : (
1980
          <Empty
1981
            title="Not enough verified evidence"
1982
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
1983
          />
1984
        )}
1985
      </section>
1986
      {!!data.learningEligibility.length && (
1987
        <Panel
1988
          title="Recent learning decisions"
1989
          subtitle="Why an interaction did or did not affect contextual history"
1990
        >
1991
          <div className="learningDecisionList">
1992
            {data.learningEligibility
1993
              .slice()
1994
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
1995
              .slice(0, 8)
1996
              .map((decision) => (
1997
                <article key={decision.decisionId}>
1998
                  <StatusPill value={decision.eligible ? 'eligible' : 'excluded'} />
1999
                  <div>
2000
                    <strong>{decision.interactionId}</strong>
2001
                    <small>
2002
                      {Math.round(Number(decision.sampleWeight ?? 0) * 100)}% weight ·{' '}
2003
                      {humanize(decision.contributionMode)} · {relativeTime(timestamp(decision))}
2004
                    </small>
2005
                    <p>{decision.reasons?.[0] ?? 'No reason supplied.'}</p>
2006
                  </div>
2007
                </article>
2008
              ))}
2009
          </div>
2010
        </Panel>
2011
      )}
2012
    </>
2013
  );
2014
}
2015
2016
function Connect({
2017
  data,
2018
  navigate,
2019
  refreshDashboard,
2020
  api,
2021
}: {
2022
  data: DashboardData;
2023
  navigate: (page: Page) => void;
2024
  refreshDashboard: () => Promise<void>;
2025
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2026
}) {
2027
  const endpoint = 'https://openclasp.vercel.app/mcp';
2028
  const [connectionType, setConnectionType] = useState<'interactive' | 'hosted'>('hosted');
2029
  const [copied, setCopied] = useState(false);
2030
  const [tokenCopied, setTokenCopied] = useState(false);
2031
  const [working, setWorking] = useState('');
2032
  const [decisionError, setDecisionError] = useState('');
2033
  const [providerError, setProviderError] = useState('');
2034
  const [providerForm, setProviderForm] = useState({
2035
    provider: 'botpress' as 'botpress' | 'custom',
2036
    agentName: '',
2037
    projectName: '',
2038
    description: '',
2039
    capabilities: '',
2040
    limitations: '',
2041
    expiresInDays: 365,
2042
  });
2043
  const [providerResult, setProviderResult] = useState<{
2044
    provider: 'botpress' | 'custom';
2045
    agent: { agentId: string; name: string; framework: string };
2046
    accessToken: { token: string; expiresAt: string };
2047
  }>();
2048
  const pending = data.setupRequests.filter((request) => request.status === 'pending');
2049
  useEffect(() => {
2050
    const poll = () => void refreshDashboard().catch(() => undefined);
2051
    poll();
2052
    const timer = window.setInterval(poll, 5000);
2053
    return () => window.clearInterval(timer);
2054
  }, [refreshDashboard]);
2055
  const decide = async (requestId: string, decision: 'approve' | 'reject') => {
2056
    setWorking(requestId);
2057
    setDecisionError('');
2058
    try {
2059
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
2060
        method: 'POST',
2061
      });
2062
      await refreshDashboard();
2063
    } catch (reason) {
2064
      setDecisionError(reason instanceof Error ? reason.message : 'Could not update setup request');
2065
    } finally {
2066
      setWorking('');
2067
    }
2068
  };
2069
  const connectHostedProvider = async (event: React.FormEvent) => {
2070
    event.preventDefault();
2071
    setWorking('hosted-provider');
2072
    setProviderError('');
2073
    try {
2074
      const list = (value: string) =>
2075
        value
2076
          .split(/[,\n]/)
2077
          .map((item) => item.trim())
2078
          .filter(Boolean);
2079
      const created = (await api('/v0.1/provider-connections', {
2080
        method: 'POST',
2081
        body: JSON.stringify({
2082
          provider: providerForm.provider,
2083
          agentName: providerForm.agentName,
2084
          projectName: providerForm.projectName,
2085
          description: providerForm.description,
2086
          capabilities: list(providerForm.capabilities),
2087
          limitations: list(providerForm.limitations),
2088
          expiresInDays: providerForm.expiresInDays,
2089
        }),
2090
      })) as {
2091
        provider: 'botpress' | 'custom';
2092
        agent: { agentId: string; name: string; framework: string };
2093
        accessToken: { token: string; expiresAt: string };
2094
      };
2095
      setProviderResult(created);
2096
      await refreshDashboard().catch(() => undefined);
2097
    } catch (reason) {
2098
      setProviderError(reason instanceof Error ? reason.message : 'Could not create connection');
2099
    } finally {
2100
      setWorking('');
2101
    }
2102
  };
2103
  return (
2104
    <>
2105
      <PageHead page="connect" />
2106
      {pending.length > 0 && (
2107
        <section className="setupRequests">
2108
          <div>
2109
            <p className="pageKicker">
2110
              <span>//</span> confirmation required
2111
            </p>
2112
            <h2>Approve agent setup</h2>
2113
            <p>
2114
              An agent proposed this identity. Confirm it before OpenClasp binds the installation.
2115
            </p>
2116
          </div>
2117
          {pending.map((request) => (
2118
            <article className="setupRequest" key={request.requestId}>
2119
              <div>
2120
                <strong>{request.agentName ?? 'Switch installation'}</strong>
2121
                <small>
2122
                  {request.projectName ?? request.existingAgentId} · {request.framework}
2123
                </small>
2124
                {!!request.capabilities?.length && (
2125
                  <div className="tags">
2126
                    {request.capabilities.slice(0, 5).map((capability: string) => (
2127
                      <span key={capability}>{capability}</span>
2128
                    ))}
2129
                  </div>
2130
                )}
2131
                {!!request.limitations?.length && (
2132
                  <small>Limitations: {request.limitations.join(' · ')}</small>
2133
                )}
2134
                <div className="automationPreview">
2135
                  <span>{request.autoPublish ? 'Public after approval' : 'Private'}</span>
2136
                  <span>
2137
                    {(request.agentMode ?? 'temporary_chat') === 'temporary_chat'
2138
                      ? 'Hosted temporary A2A · encrypted history'
2139
                      : 'Persistent runtime · direct A2A'}
2140
                  </span>
2141
                  <span>
2142
                    {request.autoAcceptPolicy === 'safe_matching'
2143
                      ? 'Auto-accept safe matches'
2144
                      : 'Review every request'}
2145
                  </span>
2146
                </div>
2147
              </div>
2148
              <div className="decisionButtons">
2149
                <button
2150
                  className="secondary"
2151
                  type="button"
2152
                  disabled={working === request.requestId}
2153
                  onClick={() => void decide(request.requestId, 'reject')}
2154
                >
2155
                  Reject
2156
                </button>
2157
                <button
2158
                  className="primary"
2159
                  type="button"
2160
                  disabled={working === request.requestId}
2161
                  onClick={() => void decide(request.requestId, 'approve')}
2162
                >
2163
                  {working === request.requestId ? 'Working…' : 'Approve & automate'}
2164
                </button>
2165
              </div>
2166
            </article>
2167
          ))}
2168
          {decisionError && (
2169
            <div className="loginError" role="alert">
2170
              {decisionError}
2171
            </div>
2172
          )}
2173
        </section>
2174
      )}
2175
      <div className="connectionTypePicker" role="tablist" aria-label="Agent connection type">
2176
        <button
2177
          type="button"
2178
          role="tab"
2179
          aria-selected={connectionType === 'hosted'}
2180
          className={connectionType === 'hosted' ? 'active' : ''}
2181
          onClick={() => setConnectionType('hosted')}
2182
        >
2183
          Hosted provider
2184
          <small>Botpress and similar platforms</small>
2185
        </button>
2186
        <button
2187
          type="button"
2188
          role="tab"
2189
          aria-selected={connectionType === 'interactive'}
2190
          className={connectionType === 'interactive' ? 'active' : ''}
2191
          onClick={() => setConnectionType('interactive')}
2192
        >
2193
          Interactive agent
2194
          <small>Codex, Cursor, and OAuth clients</small>
2195
        </button>
2196
      </div>
2197
      {connectionType === 'hosted' ? (
2198
        <section className="connectLayout providerConnectLayout">
2199
          <Panel title="Add a hosted agent" subtitle="Creates an isolated identity and credential">
2200
            {providerResult ? (
2201
              <div className="providerResult">
2202
                <div className="successBanner">
2203
                  <strong>{providerResult.agent.name} created</strong>
2204
                  <span>{providerResult.agent.agentId}</span>
2205
                </div>
2206
                <label>
2207
                  <span>MCP URL</span>
2208
                  <code>{endpoint}</code>
2209
                </label>
2210
                <label>
2211
                  <span>Agent token — copy it now; it will not be shown again</span>
2212
                  <code>{providerResult.accessToken.token}</code>
2213
                </label>
2214
                <small>
2215
                  This agent-bound token authorizes MCP and automatic runtime registration. It
2216
                  cannot connect or manage another agent.
2217
                </small>
2218
                <div className="agentActions">
2219
                  <button
2220
                    className="secondary"
2221
                    type="button"
2222
                    onClick={() => {
2223
                      void navigator.clipboard.writeText(providerResult.accessToken.token);
2224
                      setTokenCopied(true);
2225
                      window.setTimeout(() => setTokenCopied(false), 2000);
2226
                    }}
2227
                  >
2228
                    {tokenCopied ? 'Copied' : 'Copy token'}
2229
                  </button>
2230
                  <button className="primary" type="button" onClick={() => navigate('agents')}>
2231
                    View agent
2232
                  </button>
2233
                </div>
2234
              </div>
2235
            ) : (
2236
              <form
2237
                className="providerForm"
2238
                onSubmit={(event) => void connectHostedProvider(event)}
2239
              >
2240
                <label>
2241
                  <span>Provider</span>
2242
                  <select
2243
                    value={providerForm.provider}
2244
                    onChange={(event) =>
2245
                      setProviderForm((value) => ({
2246
                        ...value,
2247
                        provider: event.target.value as 'botpress' | 'custom',
2248
                      }))
2249
                    }
2250
                  >
2251
                    <option value="botpress">Botpress</option>
2252
                    <option value="custom">Custom / self-hosted</option>
2253
                  </select>
2254
                </label>
2255
                <label>
2256
                  <span>Agent name</span>
2257
                  <input
2258
                    required
2259
                    maxLength={100}
2260
                    value={providerForm.agentName}
2261
                    onChange={(event) =>
2262
                      setProviderForm((value) => ({ ...value, agentName: event.target.value }))
2263
                    }
2264
                    placeholder="Recruiting agent"
2265
                  />
2266
                </label>
2267
                <label>
2268
                  <span>Project</span>
2269
                  <input
2270
                    required
2271
                    maxLength={100}
2272
                    value={providerForm.projectName}
2273
                    onChange={(event) =>
2274
                      setProviderForm((value) => ({ ...value, projectName: event.target.value }))
2275
                    }
2276
                    placeholder="Recruiting"
2277
                  />
2278
                </label>
2279
                <label className="fullWidth">
2280
                  <span>Purpose</span>
2281
                  <textarea
2282
                    required
2283
                    maxLength={500}
2284
                    value={providerForm.description}
2285
                    onChange={(event) =>
2286
                      setProviderForm((value) => ({ ...value, description: event.target.value }))
2287
                    }
2288
                    placeholder="Matches suitable candidates with open roles"
2289
                  />
2290
                </label>
2291
                <label>
2292
                  <span>Capabilities, comma-separated</span>
2293
                  <input
2294
                    required
2295
                    value={providerForm.capabilities}
2296
                    onChange={(event) =>
2297
                      setProviderForm((value) => ({ ...value, capabilities: event.target.value }))
2298
                    }
2299
                    placeholder="candidate matching, interview coordination"
2300
                  />
2301
                </label>
2302
                <label>
2303
                  <span>Limitations, comma-separated</span>
2304
                  <input
2305
                    value={providerForm.limitations}
2306
                    onChange={(event) =>
2307
                      setProviderForm((value) => ({ ...value, limitations: event.target.value }))
2308
                    }
2309
                    placeholder="no final hiring decisions"
2310
                  />
2311
                </label>
2312
                <label>
2313
                  <span>Credential expiry</span>
2314
                  <select
2315
                    value={providerForm.expiresInDays}
2316
                    onChange={(event) =>
2317
                      setProviderForm((value) => ({
2318
                        ...value,
2319
                        expiresInDays: Number(event.target.value),
2320
                      }))
2321
                    }
2322
                  >
2323
                    <option value={30}>30 days</option>
2324
                    <option value={90}>90 days</option>
2325
                    <option value={365}>1 year</option>
2326
                  </select>
2327
                </label>
2328
                <div className="providerSubmit fullWidth">
2329
                  <p>This does not reuse or modify your Codex agent.</p>
2330
                  <button
2331
                    className="primary"
2332
                    type="submit"
2333
                    disabled={working === 'hosted-provider'}
2334
                  >
2335
                    {working === 'hosted-provider' ? 'Creating…' : 'Create agent credentials'}
2336
                  </button>
2337
                </div>
2338
                {providerError ? (
2339
                  <div className="loginError fullWidth" role="alert">
2340
                    {providerError}
2341
                  </div>
2342
                ) : null}
2343
              </form>
2344
            )}
2345
          </Panel>
2346
          <Panel
2347
            title={
2348
              providerForm.provider === 'botpress' ? 'Configure Botpress' : 'Deploy the sidecar'
2349
            }
2350
            subtitle={
2351
              providerForm.provider === 'botpress'
2352
                ? 'MCP and inbound runtime are separate connections'
2353
                : 'Runs beside an agent on any cloud'
2354
            }
2355
          >
2356
            {providerForm.provider === 'botpress' ? (
2357
              <>
2358
                <div className="connectSteps">
2359
                  <div className="connectStep">
2360
                    <b>1</b>
2361
                    <span>Add the displayed MCP URL with Bearer authentication.</span>
2362
                  </div>
2363
                  <div className="connectStep">
2364
                    <b>2</b>
2365
                    <span>
2366
                      Paste the generated <code>oc_at_…</code> agent token.
2367
                    </span>
2368
                  </div>
2369
                  <div className="connectStep">
2370
                    <b>3</b>
2371
                    <span>Install the OpenClasp Botpress runtime integration for inbound A2A.</span>
2372
                  </div>
2373
                </div>
2374
                <div className="notice providerNotice">
2375
                  MCP proves outbound tool access only. Autonomous runtime becomes connected only
2376
                  after the Botpress integration registers its webhook successfully.
2377
                </div>
2378
              </>
2379
            ) : (
2380
              <>
2381
                <div className="connectSteps">
2382
                  <div className="connectStep">
2383
                    <b>1</b>
2384
                    <span>
2385
                      Deploy <code>Dockerfile.sidecar</code> beside the agent.
2386
                    </span>
2387
                  </div>
2388
                  <div className="connectStep">
2389
                    <b>2</b>
2390
                    <span>
2391
                      Set <code>OPENCLASP_AGENT_TOKEN</code>, <code>OPENCLASP_RUNTIME_URL</code>,
2392
                      and <code>AGENT_ADAPTER_URL</code>.
2393
                    </span>
2394
                  </div>
2395
                  <div className="connectStep">
2396
                    <b>3</b>
2397
                    <span>
2398
                      The sidecar verifies and registers itself; no endpoint paste is needed.
2399
                    </span>
2400
                  </div>
2401
                </div>
2402
                <div className="notice providerNotice">
2403
                  The application implements three internal POST hooks: session-offer,
2404
                  session-activated, and message. Agent-to-agent content remains off OpenClasp.
2405
                </div>
2406
              </>
2407
            )}
2408
          </Panel>
2409
        </section>
2410
      ) : (
2411
        <section className="connectLayout">
2412
          <Panel title="Connect with OAuth" subtitle="For interactive MCP clients">
2413
            <div className="endpoint">
2414
              <code>{endpoint}</code>
2415
              <button
2416
                type="button"
2417
                onClick={() => {
2418
                  void navigator.clipboard.writeText(endpoint);
2419
                  setCopied(true);
2420
                  window.setTimeout(() => setCopied(false), 2000);
2421
                }}
2422
              >
2423
                {copied ? 'Copied' : 'Copy'}
2424
              </button>
2425
            </div>
2426
            <div className="connectSteps">
2427
              <div className="connectStep">
2428
                <b>1</b>
2429
                <span>Add this MCP URL and complete OAuth.</span>
2430
              </div>
2431
              <div className="connectStep">
2432
                <b>2</b>
2433
                <span>
2434
                  Tell the agent: <code>Set yourself up on OpenClasp</code>.
2435
                </span>
2436
              </div>
2437
              <div className="connectStep">
2438
                <b>3</b>
2439
                <span>Approve the proposed identity and mode here.</span>
2440
              </div>
2441
            </div>
2442
          </Panel>
2443
          <Panel title="Interactive clients" subtitle="Best for temporary user-driven sessions">
2444
            <ul className="checkList">
2445
              <li>OAuth opens in the browser</li>
2446
              <li>Each installation is explicitly approved</li>
2447
              <li>Temporary chats receive hosted A2A history</li>
2448
              <li>Persistent runtimes can switch to direct A2A</li>
2449
            </ul>
2450
          </Panel>
2451
        </section>
2452
      )}
2453
    </>
2454
  );
2455
}
2456
2457
function SettingsPage({
2458
  settings,
2459
  setSettings,
2460
  api,
2461
}: {
2462
  settings: Settings;
2463
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
2464
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2465
}) {
2466
  const [saving, setSaving] = useState(false);
2467
  const [saved, setSaved] = useState(false);
2468
  const [error, setError] = useState('');
2469
  const save = useCallback(async () => {
2470
    setSaving(true);
2471
    setSaved(false);
2472
    setError('');
2473
    try {
2474
      const updated = (await api('/v0.1/settings', {
2475
        method: 'PUT',
2476
        body: JSON.stringify(settings),
2477
      })) as Settings;
2478
      setSettings(updated);
2479
      setSaved(true);
2480
    } catch (reason) {
2481
      setError(reason instanceof Error ? reason.message : 'Could not save settings');
2482
    } finally {
2483
      setSaving(false);
2484
    }
2485
  }, [api, settings, setSettings]);
2486
  return (
2487
    <>
2488
      <PageHead page="settings" />
2489
      <section className="settingsCard">
2490
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
2491
          <input
2492
            value={settings.displayName}
2493
            onChange={(event) =>
2494
              setSettings((value) => ({ ...value, displayName: event.target.value }))
2495
            }
2496
            placeholder="Team or operator name"
2497
          />
2498
        </Setting>
2499
        <Setting
2500
          label="Network contribution"
2501
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
2502
        >
2503
          <Toggle
2504
            checked={settings.contributionEnabled}
2505
            label="Network contribution"
2506
            onChange={(checked) =>
2507
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
2508
            }
2509
          />
2510
        </Setting>
2511
        <Setting
2512
          label="Evidence sharing"
2513
          description="Controls when permitted evidence references may be shared with interaction participants."
2514
        >
2515
          <select
2516
            value={settings.evidenceSharing}
2517
            onChange={(event) =>
2518
              setSettings((value) => ({
2519
                ...value,
2520
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
2521
              }))
2522
            }
2523
          >
2524
            <option value="never">Never</option>
2525
            <option value="ask">Ask every time</option>
2526
            <option value="contract_only">When contract permits</option>
2527
          </select>
2528
        </Setting>
2529
        <Setting
2530
          label="Structured record retention"
2531
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
2532
        >
2533
          <select
2534
            value={settings.retentionDays}
2535
            onChange={(event) =>
2536
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
2537
            }
2538
          >
2539
            <option value={7}>7 days</option>
2540
            <option value={30}>30 days</option>
2541
            <option value={90}>90 days</option>
2542
            <option value={365}>1 year</option>
2543
            <option value={0}>No automatic deletion</option>
2544
          </select>
2545
        </Setting>
2546
        <Setting
2547
          label="Raw agent messages"
2548
          description="Persistent runtimes remain direct and are not stored. Temporary chat messages pass through OpenClasp and are encrypted at rest for 30 days."
2549
        >
2550
          <span className="locked">MODE DEPENDENT</span>
2551
        </Setting>
2552
        <div className="saveRow">
2553
          <button className="primary" type="button" onClick={() => void save()} disabled={saving}>
2554
            {saving ? 'Saving…' : 'Save settings'}
2555
          </button>
2556
          {saved && <span role="status">Saved</span>}
2557
          {error && (
2558
            <span className="loginError" role="alert">
2559
              {error}
2560
            </span>
2561
          )}
2562
        </div>
2563
      </section>
2564
    </>
2565
  );
2566
}
2567
2568
function PageHead({
2569
  page,
2570
  action,
2571
  onAction,
2572
}: {
2573
  page: Page;
2574
  action?: string;
2575
  onAction?: () => void;
2576
}) {
2577
  const meta = pageMeta[page];
2578
  return (
2579
    <header className="pageHead">
2580
      <div>
2581
        <p className="pageKicker">
2582
          <span>//</span> {meta.eyebrow.toLowerCase()}
2583
        </p>
2584
        <h1>{meta.title}</h1>
2585
        <p className="lede">{meta.lede}</p>
2586
      </div>
2587
      {action && (
2588
        <button className="primary" type="button" onClick={onAction}>
2589
          {action.toLowerCase()} <span aria-hidden="true">→</span>
2590
        </button>
2591
      )}
2592
    </header>
2593
  );
2594
}
2595
function Metric({
2596
  label,
2597
  value,
2598
  note,
2599
  warn,
2600
}: {
2601
  label: string;
2602
  value: number;
2603
  note: string;
2604
  warn?: boolean;
2605
}) {
2606
  return (
2607
    <article className={warn ? 'metric warn' : 'metric'} title={note}>
2608
      <span>{label}</span>
2609
      <strong>{value}</strong>
2610
      <small>{note}</small>
2611
    </article>
2612
  );
2613
}
2614
function Panel({
2615
  title,
2616
  subtitle,
2617
  children,
2618
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
2619
  return (
2620
    <section className="panel">
2621
      <div className="panelHead">
2622
        <h2 className="panelKicker">
2623
          <span>//</span> {title.toLowerCase()}
2624
        </h2>
2625
        {subtitle ? <p>{subtitle}</p> : null}
2626
      </div>
2627
      {children}
2628
    </section>
2629
  );
2630
}
2631
function Timeline({ events }: { events: Record<string, any>[] }) {
2632
  return events.length ? (
2633
    <div className="timeline">
2634
      {events.map((event) => (
2635
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
2636
      ))}
2637
    </div>
2638
  ) : (
2639
    <Empty
2640
      title="Nothing recorded yet"
2641
      text="Activity appears after your connected agents submit signed structured events."
2642
    />
2643
  );
2644
}
2645
function HistoryRow({ item }: { item: Record<string, any> }) {
2646
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
2647
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
2648
  return (
2649
    <article className="historyRow">
2650
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
2651
      <div>
2652
        <strong>{kind}</strong>
2653
        <small>
2654
          {id} · {new Date(timestamp(item)).toLocaleString()}
2655
        </small>
2656
      </div>
2657
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
2658
    </article>
2659
  );
2660
}
2661
function AgentCard({
2662
  agent,
2663
  projectName,
2664
  published,
2665
  working,
2666
  onSave,
2667
  runtime,
2668
  runtimeWorking,
2669
  deleteWorking,
2670
  onRuntime,
2671
  onDisableRuntime,
2672
  accessTokens,
2673
  accessTokenWorking,
2674
  onRevokeAccessToken,
2675
  onDelete,
2676
}: {
2677
  agent: Record<string, any>;
2678
  projectName?: string;
2679
  published: boolean;
2680
  working: boolean;
2681
  onSave: (value: {
2682
    autoPublish: boolean;
2683
    autoAcceptPolicy: 'off' | 'safe_matching';
2684
    autoAcceptTaskCategories: string[];
2685
  }) => Promise<boolean>;
2686
  runtime: Record<string, any> | undefined;
2687
  runtimeWorking: boolean;
2688
  deleteWorking: boolean;
2689
  onRuntime: (endpoint: string) => void;
2690
  onDisableRuntime: () => void;
2691
  accessTokens: Record<string, any>[];
2692
  accessTokenWorking: boolean;
2693
  onRevokeAccessToken: (tokenId: string) => Promise<void>;
2694
  onDelete: () => void;
2695
}) {
2696
  const [editing, setEditing] = useState(false);
2697
  const [autoPublish, setAutoPublish] = useState(Boolean(agent.autoPublish ?? published));
2698
  const [autoAcceptPolicy, setAutoAcceptPolicy] = useState<'off' | 'safe_matching'>(
2699
    agent.autoAcceptPolicy === 'safe_matching' ? 'safe_matching' : 'off',
2700
  );
2701
  const [categories, setCategories] = useState<string>(
2702
    (agent.autoAcceptTaskCategories?.length
2703
      ? agent.autoAcceptTaskCategories
2704
      : (agent.capabilities ?? [])
2705
    ).join(', '),
2706
  );
2707
  const [runtimeEndpoint, setRuntimeEndpoint] = useState(String(runtime?.endpoint ?? ''));
2708
  const mode = agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
2709
  const temporary = mode === 'temporary_chat';
2710
  const ready = published && (temporary || runtime?.status === 'verified');
2711
  const online = agent.presence?.status === 'online';
2712
  const providerConnected = accessTokens.length > 0;
2713
  const endpoint = String(
2714
    temporary
2715
      ? `/a2a/temporary/${encodeURIComponent(agent.agentId)}`
2716
      : (runtime?.a2aEndpoint ?? agent.a2aEndpoint ?? 'Connect a runtime first'),
2717
  );
2718
  const identityLabel = agent.revoked
2719
    ? 'REVOKED'
2720
    : agent.identityMode === 'owner_managed'
2721
      ? 'OWNER VERIFIED'
2722
      : agent.identityMode === 'oauth_installation'
2723
        ? 'AUTHENTICATED'
2724
        : 'VERIFIED';
2725
  return (
2726
    <article className="agentCard">
2727
      <div className="agentTop">
2728
        <span className="agentGlyph">
2729
          <Bot />
2730
        </span>
2731
        <div className="agentBadges">
2732
          <b className={online && temporary ? 'onlineBadge' : 'offlineBadge'}>
2733
            {temporary
2734
              ? online
2735
                ? 'CHAT ACTIVE'
2736
                : 'CHAT IDLE'
2737
              : runtime?.status === 'verified'
2738
                ? 'ENDPOINT VERIFIED'
2739
                : providerConnected
2740
                  ? 'TOKEN ONLY'
2741
                  : 'ENDPOINT MISSING'}
2742
          </b>
2743
          <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
2744
          <b className={ready ? 'readyBadge' : 'needsBadge'}>{ready ? 'READY' : 'SETUP NEEDED'}</b>
2745
        </div>
2746
      </div>
2747
      <h2>{agent.name ?? agent.agentId}</h2>
2748
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
2749
      <div className="tags">
2750
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
2751
          <span key={capability}>{capability}</span>
2752
        ))}
2753
      </div>
2754
      <small>
2755
        {agent.framework ? `${agent.framework} · ` : ''}
2756
        {agent.identityMode === 'owner_managed'
2757
          ? 'Owner-created · '
2758
          : agent.identityMode === 'oauth_installation'
2759
            ? 'OAuth-bound · '
2760
            : 'Ed25519 · '}
2761
        Created {new Date(agent.createdAt).toLocaleDateString()}
2762
      </small>
2763
      <small>
2764
        {agent.presence?.lastSeenAt
2765
          ? `Last agent activity ${new Date(agent.presence.lastSeenAt).toLocaleString()}`
2766
          : 'No agent activity recorded yet'}
2767
      </small>
2768
      {published ? (
2769
        <div className="agentPublicLinks">
2770
          <a href={`/a/${encodeURIComponent(agent.agentId)}`} target="_blank" rel="noreferrer">
2771
            Verified public profile
2772
          </a>
2773
          <a
2774
            href={`/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`}
2775
            target="_blank"
2776
            rel="noreferrer"
2777
          >
2778
            A2A Agent Card
2779
          </a>
2780
        </div>
2781
      ) : null}
2782
      <div className="automationSummary">
2783
        <span>{published ? 'Public discovery' : 'Private'}</span>
2784
        <span>{temporary ? 'Hosted temporary A2A' : 'Direct agent-owned A2A'}</span>
2785
        <span>
2786
          {agent.autoAcceptPolicy === 'safe_matching' ? 'Safe tasks automatic' : 'Manual approval'}
2787
        </span>
2788
      </div>
2789
      <div className="runtimeBox">
2790
        <div>
2791
          <strong>{temporary ? 'Temporary chat endpoint' : 'Autonomous runtime'}</strong>
2792
          <span
2793
            className={
2794
              temporary || runtime?.status === 'verified' ? 'runtimeLive' : 'runtimeMissing'
2795
            }
2796
          >
2797
            {temporary
2798
              ? 'HOSTED BY OPENCLASP'
2799
              : runtime?.status === 'verified'
2800
                ? 'VERIFIED'
2801
                : 'NOT CONNECTED'}
2802
          </span>
2803
        </div>
2804
        <p>
2805
          {temporary
2806
            ? 'OpenClasp receives A2A for this temporary identity and encrypts its history at rest. Connect a runtime below to switch to direct A2A.'
2807
            : runtime?.status === 'verified'
2808
              ? 'The agent runtime registered itself successfully. Live message bodies travel directly between agents.'
2809
              : providerConnected
2810
                ? 'MCP is connected, but inbound A2A is not. Install the provider connector or deploy the sidecar; it registers this endpoint automatically.'
2811
                : 'Install a provider connector, deploy the sidecar, or register a native A2A endpoint.'}
2812
        </p>
2813
        <input
2814
          type="url"
2815
          value={runtimeEndpoint}
2816
          onChange={(event) => setRuntimeEndpoint(event.target.value)}
2817
          placeholder="https://agent.example.com/openclasp"
2818
        />
2819
        <small>
2820
          Manual registration is only for a runtime that already implements OpenClasp A2A.
2821
        </small>
2822
        <div className="agentActions">
2823
          {runtime?.status === 'verified' ? (
2824
            <button
2825
              className="secondary"
2826
              type="button"
2827
              disabled={runtimeWorking}
2828
              onClick={onDisableRuntime}
2829
            >
2830
              Disable runtime
2831
            </button>
2832
          ) : null}
2833
          <button
2834
            className="primary"
2835
            type="button"
2836
            disabled={runtimeWorking || !runtimeEndpoint.trim()}
2837
            onClick={() => onRuntime(runtimeEndpoint.trim())}
2838
          >
2839
            {runtimeWorking ? 'Verifying…' : runtime ? 'Rotate connection' : 'Verify & connect'}
2840
          </button>
2841
        </div>
2842
        {runtime?.lastError ? <small>Last session error: {runtime.lastError}</small> : null}
2843
      </div>
2844
      {accessTokens.length ? (
2845
        <div className="accessTokenBox">
2846
          <div className="accessTokenHead">
2847
            <div>
2848
              <strong>Provider connections</strong>
2849
              <span>{accessTokens.length} ACTIVE</span>
2850
            </div>
2851
            <p>Created through Connect. Revoke a provider here if its credential is exposed.</p>
2852
          </div>
2853
          <div className="tokenList">
2854
            {accessTokens.map((token) => (
2855
              <div key={token.tokenId}>
2856
                <span>
2857
                  <strong>{token.name}</strong>
2858
                  <small>
2859
                    Created {new Date(token.createdAt).toLocaleDateString()} · expires{' '}
2860
                    {new Date(token.expiresAt).toLocaleDateString()}
2861
                    {token.lastUsedAt
2862
                      ? ` · used ${new Date(token.lastUsedAt).toLocaleString()}`
2863
                      : ' · never used'}
2864
                  </small>
2865
                </span>
2866
                <button
2867
                  className="secondary dangerButton"
2868
                  type="button"
2869
                  disabled={accessTokenWorking}
2870
                  onClick={() => {
2871
                    if (window.confirm(`Revoke “${String(token.name)}”? Botpress will disconnect.`))
2872
                      void onRevokeAccessToken(String(token.tokenId));
2873
                  }}
2874
                >
2875
                  Revoke
2876
                </button>
2877
              </div>
2878
            ))}
2879
          </div>
2880
        </div>
2881
      ) : null}
2882
      {editing ? (
2883
        <div className="automationForm">
2884
          <label>
2885
            <span>
2886
              {temporary ? 'OpenClasp temporary A2A endpoint' : 'Agent-owned A2A endpoint'}
2887
            </span>
2888
            <input type="url" value={endpoint} readOnly />
2889
          </label>
2890
          <label>
2891
            <span>Invitation policy</span>
2892
            <select
2893
              value={autoAcceptPolicy}
2894
              onChange={(event) =>
2895
                setAutoAcceptPolicy(event.target.value as 'off' | 'safe_matching')
2896
              }
2897
            >
2898
              <option value="safe_matching">Auto-accept safe matches</option>
2899
              <option value="off">Review every request</option>
2900
            </select>
2901
          </label>
2902
          <label>
2903
            <span>Safe task categories</span>
2904
            <input
2905
              value={categories}
2906
              onChange={(event) => setCategories(event.target.value)}
2907
              placeholder="research, planning, coding"
2908
            />
2909
          </label>
2910
          <label className="checkControl">
2911
            <input
2912
              type="checkbox"
2913
              checked={autoPublish}
2914
              onChange={(event) => setAutoPublish(event.target.checked)}
2915
            />
2916
            <span>Publish and keep Agent Card updated</span>
2917
          </label>
2918
          <div className="agentActions">
2919
            <button className="secondary" type="button" onClick={() => setEditing(false)}>
2920
              Cancel
2921
            </button>
2922
            <button
2923
              className="primary"
2924
              type="button"
2925
              disabled={working || agent.status === 'revoked'}
2926
              onClick={() =>
2927
                void onSave({
2928
                  autoPublish,
2929
                  autoAcceptPolicy,
2930
                  autoAcceptTaskCategories: categories
2931
                    .split(',')
2932
                    .map((value) => value.trim())
2933
                    .filter(Boolean),
2934
                }).then((ok) => {
2935
                  if (ok) setEditing(false);
2936
                })
2937
              }
2938
            >
2939
              {working ? 'Saving…' : 'Save automation'}
2940
            </button>
2941
          </div>
2942
        </div>
2943
      ) : (
2944
        <div className="agentActions agentManagementActions">
2945
          <button
2946
            className="secondary dangerButton"
2947
            type="button"
2948
            disabled={working || runtimeWorking || deleteWorking}
2949
            onClick={onDelete}
2950
          >
2951
            {deleteWorking ? 'Deleting…' : 'Delete agent'}
2952
          </button>
2953
          <button
2954
            className="secondary"
2955
            type="button"
2956
            disabled={working || runtimeWorking || deleteWorking || agent.status === 'revoked'}
2957
            onClick={() => setEditing(true)}
2958
          >
2959
            Configure automation
2960
          </button>
2961
        </div>
2962
      )}
2963
    </article>
2964
  );
2965
}
2966
function Profile({
2967
  profile,
2968
  expanded,
2969
  deltas = [],
2970
  showHeader = true,
2971
}: {
2972
  profile: Record<string, any>;
2973
  expanded?: boolean;
2974
  deltas?: Record<string, any>[];
2975
  showHeader?: boolean;
2976
}) {
2977
  const dimensions = [
2978
    ['Completion', profile.completion],
2979
    ['Acceptance', profile.acceptance],
2980
    ['Specification', profile.specification],
2981
    ['Scope', profile.scope],
2982
    ['Evidence', profile.evidence],
2983
    ...(expanded
2984
      ? [
2985
          ['Communication', profile.communication],
2986
          ['Deadline', profile.deadline],
2987
          ['Dispute-free', 1 - Number(profile.disputes ?? 0)],
2988
        ]
2989
      : []),
2990
  ] as [string, number][];
2991
  const latestDelta = deltas[0];
2992
  return (
2993
    <article className="profile">
2994
      <div className={showHeader ? 'profileHead' : 'profileHead profileHeadCompact'}>
2995
        {showHeader ? (
2996
          <div>
2997
            <strong>{profile.agentId}</strong>
2998
            <small>
2999
              {humanize(profile.taskCategory)} · version {profile.agentVersion ?? 'unknown'}
3000
            </small>
3001
          </div>
3002
        ) : (
3003
          <small>
3004
            Effective evidence{' '}
3005
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
3006
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
3007
          </small>
3008
        )}
3009
        <span className="sampleBadge">
3010
          {profile.sampleSize ?? 0} outcome{Number(profile.sampleSize) === 1 ? '' : 's'}
3011
        </span>
3012
      </div>
3013
      {showHeader ? (
3014
        <div className="profileEvidenceLine">
3015
          <small>
3016
            Effective evidence{' '}
3017
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
3018
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
3019
          </small>
3020
        </div>
3021
      ) : null}
3022
      {dimensions.map(([label, value]) => (
3023
        <Meter key={label} label={label} value={Number(value ?? 0.5)} />
3024
      ))}
3025
      {latestDelta ? (
3026
        <div className="profileDelta">
3027
          <span>Latest evidence impact</span>
3028
          <div>
3029
            {Object.entries(latestDelta.dimensionDeltas ?? {})
3030
              .filter(([, value]) => Math.abs(Number(value)) >= 0.001)
3031
              .slice(0, 5)
3032
              .map(([dimension, value]) => (
3033
                <b className={Number(value) >= 0 ? 'positive' : 'negative'} key={dimension}>
3034
                  {humanize(dimension)} {Number(value) >= 0 ? '+' : ''}
3035
                  {(Number(value) * 100).toFixed(1)}
3036
                </b>
3037
              ))}
3038
          </div>
3039
        </div>
3040
      ) : null}
3041
    </article>
3042
  );
3043
}
3044
function Meter({ label, value }: { label: string; value: number }) {
3045
  const bounded = Math.max(0, Math.min(1, value));
3046
  return (
3047
    <div className="meter">
3048
      <span>{label}</span>
3049
      <i>
3050
        <b style={{ width: `${Math.round(bounded * 100)}%` }} />
3051
      </i>
3052
      <strong>{Math.round(bounded * 100)}%</strong>
3053
    </div>
3054
  );
3055
}
3056
function Empty({
3057
  title,
3058
  text,
3059
  action,
3060
  onAction,
3061
}: {
3062
  title: string;
3063
  text: string;
3064
  action?: string;
3065
  onAction?: () => void;
3066
}) {
3067
  return (
3068
    <div className="empty">
3069
      <Bot />
3070
      <strong>{title}</strong>
3071
      <p>{text}</p>
3072
      {action && (
3073
        <button className="secondary" type="button" onClick={onAction}>
3074
          {action}
3075
        </button>
3076
      )}
3077
    </div>
3078
  );
3079
}
3080
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
3081
  return (
3082
    <button className="textButton" type="button" onClick={onClick}>
3083
      {children}
3084
    </button>
3085
  );
3086
}
3087
function Setting({
3088
  label,
3089
  description,
3090
  children,
3091
}: React.PropsWithChildren<{ label: string; description: string }>) {
3092
  return (
3093
    <div className="setting">
3094
      <div>
3095
        <strong>{label}</strong>
3096
        <p>{description}</p>
3097
      </div>
3098
      <div>{children}</div>
3099
    </div>
3100
  );
3101
}
3102
function Toggle({
3103
  checked,
3104
  onChange,
3105
  label,
3106
}: {
3107
  checked: boolean;
3108
  onChange: (checked: boolean) => void;
3109
  label?: string;
3110
}) {
3111
  return (
3112
    <button
3113
      className={checked ? 'toggle on' : 'toggle'}
3114
      type="button"
3115
      role="switch"
3116
      aria-checked={checked}
3117
      aria-label={label}
3118
      onClick={() => onChange(!checked)}
3119
    >
3120
      <span />
3121
    </button>
3122
  );
3123
}
3124
function Loading({ compact }: { compact?: boolean }) {
3125
  return (
3126
    <div className={compact ? 'loading compact' : 'loading'}>
3127
      <span className="spinner" aria-hidden="true" />
3128
      <p>Verifying session…</p>
3129
    </div>
3130
  );
3131
}
3132
3133
function GoogleMark() {
3134
  return (
3135
    <svg viewBox="0 0 24 24" aria-hidden="true">
3136
      <path
3137
        fill="#4285F4"
3138
        d="M22.6 12.2c0-.8-.1-1.6-.2-2.3H12v4.4h5.9c-.3 1.4-1 2.6-2.2 3.4v2.8h3.6c2.1-1.9 3.3-4.8 3.3-8.3"
3139
      />
3140
      <path
3141
        fill="#34A853"
3142
        d="M12 23c3 0 5.5-1 7.3-2.7l-3.6-2.8c-1 .7-2.3 1.1-3.7 1.1-2.8 0-5.2-1.9-6.1-4.4H2.2v2.9C4 20.5 7.7 23 12 23"
3143
      />
3144
      <path
3145
        fill="#FBBC05"
3146
        d="M5.9 14.2c-.2-.7-.4-1.4-.4-2.2s.1-1.5.4-2.2V7H2.2C1.4 8.6 1 10.3 1 12s.4 3.4 1.2 5z"
3147
      />
3148
      <path
3149
        fill="#EA4335"
3150
        d="M12 4.8c1.6 0 3.1.6 4.2 1.7l3.2-3.2C17.5 1.5 15 0.5 12 .5 7.7.5 4 3 2.2 7l3.7 2.9C6.8 6.7 9.2 4.8 12 4.8"
3151
      />
3152
    </svg>
3153
  );
3154
}
3155
3156
function GitHubMark() {
3157
  return (
3158
    <svg viewBox="0 0 24 24" aria-hidden="true">
3159
      <path
3160
        fill="currentColor"
3161
        d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.7c-2.8.6-3.4-1.2-3.4-1.2-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.4 1.1 3 .8.1-.6.4-1.1.6-1.3-2.2-.3-4.6-1.1-4.6-5a3.9 3.9 0 0 1 1-2.7 3.6 3.6 0 0 1 .1-2.6s.8-.3 2.7 1a9.3 9.3 0 0 1 5 0c1.9-1.3 2.7-1 2.7-1a3.6 3.6 0 0 1 .1 2.6 3.9 3.9 0 0 1 1 2.7c0 3.9-2.3 4.7-4.6 5 .4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5A10 10 0 0 0 12 2"
3162
      />
3163
    </svg>
3164
  );
3165
}
3166
3167
function humanize(value: unknown) {
3168
  const text = String(value ?? '')
3169
    .replaceAll('_', ' ')
3170
    .trim();
3171
  return text ? text[0]!.toUpperCase() + text.slice(1) : 'Unknown';
3172
}
3173
3174
function formatDate(value: string) {
3175
  const date = new Date(value);
3176
  return Number.isNaN(date.getTime()) ? 'Time unavailable' : date.toLocaleString();
3177
}
3178
3179
function relativeTime(value: string) {
3180
  const milliseconds = Date.now() - Date.parse(value);
3181
  if (!Number.isFinite(milliseconds)) return 'Time unavailable';
3182
  const minutes = Math.round(Math.abs(milliseconds) / 60_000);
3183
  const future = milliseconds < 0;
3184
  const phrase =
3185
    minutes < 1
3186
      ? 'just now'
3187
      : minutes < 60
3188
        ? `${minutes}m`
3189
        : minutes < 1_440
3190
          ? `${Math.round(minutes / 60)}h`
3191
          : `${Math.round(minutes / 1_440)}d`;
3192
  return minutes < 1 ? phrase : future ? `in ${phrase}` : `${phrase} ago`;
3193
}
3194
3195
function feedbackState(requests: Record<string, any>[]) {
3196
  if (!requests.length) return 'Starts after a completion report';
3197
  const pending = requests.filter((request) => request.status === 'pending').length;
3198
  if (pending) return `${pending} response${pending === 1 ? '' : 's'} still pending`;
3199
  const expired = requests.filter((request) => request.status === 'expired').length;
3200
  return expired
3201
    ? `${expired} request${expired === 1 ? '' : 's'} timed out`
3202
    : 'Bilateral feedback closed';
3203
}
3204
3205
function StatusPill({ value }: { value: string }) {
3206
  const normalized = String(value).toLowerCase();
3207
  const tone = [
3208
    'success',
3209
    'active',
3210
    'ready',
3211
    'met',
3212
    'eligible',
3213
    'attested',
3214
    'allow',
3215
    'completed',
3216
  ].includes(normalized)
3217
    ? 'good'
3218
    : ['failure', 'denied', 'deny', 'missed', 'rejected', 'violation'].some((item) =>
3219
          normalized.includes(item),
3220
        )
3221
      ? 'danger'
3222
      : ['partial', 'pending', 'provisional', 'challenge', 'conflict', 'expired', 'excluded'].some(
3223
            (item) => normalized.includes(item),
3224
          )
3225
        ? 'warn'
3226
        : 'neutral';
3227
  return (
3228
    <span className={`statusPill ${tone}`}>
3229
      <i />
3230
      {humanize(value)}
3231
    </span>
3232
  );
3233
}
3234
3235
function timestamp(item: Record<string, any>) {
3236
  return String(
3237
    item.timestamp ??
3238
      item.generatedAt ??
3239
      item.decidedAt ??
3240
      item.appliedAt ??
3241
      item.submittedAt ??
3242
      item.completedAt ??
3243
      item.activatedAt ??
3244
      item.updatedAt ??
3245
      item.requestedAt ??
3246
      item.createdAt ??
3247
      new Date(0).toISOString(),
3248
  );
3249
}
3250
3251
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
3252
  throw new Error('Auth0 public configuration is incomplete');
3253
createRoot(document.getElementById('root')!).render(
3254
  <React.StrictMode>
3255
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
3256
  </React.StrictMode>,
3257
);