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

Viewing files at commit 89b58109a0e6Commit detailsView latest

apps/dashboard/src/main.tsx 4038 lines · 146.7 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import {
4
  ArrowRight,
5
  Check,
6
  ChevronDown,
7
  Circle,
8
  Cloud,
9
  Copy,
10
  Download,
11
  ExternalLink,
12
  Moon,
13
  Search,
14
  Share2,
15
  ShieldCheck,
16
  Sun,
17
  X,
18
} from 'lucide-react';
19
import { AgentMark, FrameCorners } from '@/components/agent-mark';
20
import { AppShell } from '@/components/app-shell';
21
import { ClaspMark } from '@/components/clasp-mark';
22
import { FirstRunGuide } from '@/components/first-run-guide';
23
import { LandingBackdrop, LandingDiagram } from '@/components/landing-scene';
24
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
25
import { Input } from '@/components/ui/input';
26
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
27
import { pageMeta, pages, type Page } from '@/lib/navigation';
28
import {
29
  applyPreviewRequest,
30
  createPreviewData,
31
  defaultPreviewSettings,
32
  disablePreview,
33
  enablePreview,
34
  isPreviewActive,
35
  previewSession,
36
} from './preview.js';
37
import './styles.css';
38
39
declare const __AUTH0_DOMAIN__: string;
40
declare const __AUTH0_CLIENT_ID__: string;
41
declare const __AUTH0_AUDIENCE__: string;
42
declare const __OPENCLASP_PUBLIC_URL__: string;
43
44
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
45
type AuthSession = { user: Auth0User };
46
type AuthTransaction = { state: string; nonce: string; verifier: string };
47
type Theme = 'dark' | 'light';
48
49
const authTransactionKey = 'openclasp.auth0.transaction';
50
const themeKey = 'openclasp.theme.v1';
51
const landingCapabilities = [
52
  'publisher authenticated',
53
  'contextual intelligence',
54
  'attested agreements',
55
  'direct A2A',
56
  'authenticated outcomes',
57
  'behavioural profiles',
58
];
59
60
type DashboardData = {
61
  agents: Record<string, any>[];
62
  projects: Record<string, any>[];
63
  installations: Record<string, any>[];
64
  setupRequests: Record<string, any>[];
65
  publications: Record<string, any>[];
66
  interactions: Record<string, any>[];
67
  federatedInteractions: Record<string, any>[];
68
  liveSessions: Record<string, any>[];
69
  events: Record<string, any>[];
70
  conflicts: Record<string, any>[];
71
  receipts: Record<string, any>[];
72
  profiles: Record<string, any>[];
73
  counterpartyBriefs: Record<string, any>[];
74
  completionReports: Record<string, any>[];
75
  feedbackRequests: Record<string, any>[];
76
  interactionFeedback: Record<string, any>[];
77
  interactionConclusions: Record<string, any>[];
78
  learningEligibility: Record<string, any>[];
79
  profileDeltas: Record<string, any>[];
80
  intelligenceSummaries: Record<string, any>[];
81
  runtimes: Record<string, any>[];
82
  accessTokens: Record<string, any>[];
83
};
84
85
type Settings = {
86
  displayName: string;
87
  contributionEnabled: boolean;
88
  rawConversationsStored: false;
89
};
90
91
const emptyData: DashboardData = {
92
  agents: [],
93
  projects: [],
94
  installations: [],
95
  setupRequests: [],
96
  publications: [],
97
  interactions: [],
98
  federatedInteractions: [],
99
  liveSessions: [],
100
  events: [],
101
  conflicts: [],
102
  receipts: [],
103
  profiles: [],
104
  counterpartyBriefs: [],
105
  completionReports: [],
106
  feedbackRequests: [],
107
  interactionFeedback: [],
108
  interactionConclusions: [],
109
  learningEligibility: [],
110
  profileDeltas: [],
111
  intelligenceSummaries: [],
112
  runtimes: [],
113
  accessTokens: [],
114
};
115
const defaultSettings: Settings = {
116
  displayName: '',
117
  contributionEnabled: false,
118
  rawConversationsStored: false,
119
};
120
121
function normalizeDashboard(value: unknown): DashboardData {
122
  return { ...emptyData, ...(value as Partial<DashboardData>) };
123
}
124
125
function base64Url(bytes: Uint8Array) {
126
  let binary = '';
127
  for (const byte of bytes) binary += String.fromCharCode(byte);
128
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
129
}
130
131
function randomValue(size = 32) {
132
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
133
}
134
135
async function beginAuth(provider: 'google' | 'github') {
136
  const verifier = randomValue(48);
137
  const digest = await crypto.subtle.digest(
138
    'SHA-256',
139
    new TextEncoder().encode(verifier) as BufferSource,
140
  );
141
  const transaction: AuthTransaction = { state: randomValue(), nonce: randomValue(), verifier };
142
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
143
  const parameters = new URLSearchParams({
144
    client_id: __AUTH0_CLIENT_ID__,
145
    redirect_uri: `${location.origin}/sso-callback`,
146
    response_type: 'code',
147
    scope: 'openid profile email',
148
    audience: __AUTH0_AUDIENCE__,
149
    state: transaction.state,
150
    nonce: transaction.nonce,
151
    code_challenge: base64Url(new Uint8Array(digest)),
152
    code_challenge_method: 'S256',
153
    connection: provider === 'google' ? 'google-oauth2' : 'github',
154
  });
155
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
156
}
157
158
async function signOut(preview: boolean) {
159
  if (preview) {
160
    disablePreview();
161
    location.replace('/login');
162
    return;
163
  }
164
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
165
  const parameters = new URLSearchParams({
166
    client_id: __AUTH0_CLIENT_ID__,
167
    returnTo: `${location.origin}/login`,
168
  });
169
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
170
}
171
172
async function remoteApi(path: string, init?: RequestInit) {
173
  const headers = new Headers(init?.headers);
174
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
175
  return fetch(path, {
176
    ...init,
177
    credentials: 'same-origin',
178
    headers,
179
  }).then(async (response) => {
180
    if (!response.ok) {
181
      const text = await response.text();
182
      let message = text || `Request failed: ${response.status}`;
183
      try {
184
        const value = JSON.parse(text) as { error?: unknown };
185
        if (typeof value.error === 'string') message = value.error;
186
      } catch {
187
        // Preserve non-JSON error text from proxies and platform failures.
188
      }
189
      throw new Error(message);
190
    }
191
    return response.json();
192
  });
193
}
194
195
function route(): Page {
196
  const value = location.pathname.slice(1) || 'dashboard';
197
  if ((pages as readonly string[]).includes(value)) return value as Page;
198
  return 'dashboard';
199
}
200
201
function initialTheme(): Theme {
202
  const saved = localStorage.getItem(themeKey);
203
  if (saved === 'dark' || saved === 'light') return saved;
204
  return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
205
}
206
207
function App() {
208
  const [session, setSession] = useState<AuthSession | null>();
209
  const [preview, setPreview] = useState(false);
210
  const [theme, setTheme] = useState<Theme>(initialTheme);
211
  const [page, setPage] = useState<Page>(route());
212
  const [data, setData] = useState<DashboardData>(emptyData);
213
  const [settings, setSettings] = useState<Settings>(defaultSettings);
214
  const [loading, setLoading] = useState(true);
215
  const [error, setError] = useState('');
216
  const api = useCallback(
217
    async (path: string, init?: RequestInit) => {
218
      if (preview) {
219
        const next = applyPreviewRequest(data, settings, path, init);
220
        setData(next.data);
221
        setSettings(next.settings);
222
        return next.result;
223
      }
224
      return remoteApi(path, init);
225
    },
226
    [data, preview, settings],
227
  );
228
  const refreshDashboard = useCallback(async () => {
229
    if (preview) return;
230
    setData(normalizeDashboard(await remoteApi('/v0.1/dashboard')));
231
  }, [preview]);
232
233
  useEffect(() => {
234
    document.title = session
235
      ? `${pageMeta[page].title} · OpenClasp`
236
      : session === null
237
        ? 'OpenClasp · Trust for agent communication'
238
        : 'OpenClasp';
239
  }, [page, session]);
240
241
  useEffect(() => {
242
    let cancelled = false;
243
    const boot = async () => {
244
      if (import.meta.env.DEV && isPreviewActive()) {
245
        if (cancelled) return;
246
        setPreview(true);
247
        setSession(previewSession);
248
        setData(createPreviewData());
249
        setSettings(defaultPreviewSettings);
250
        setLoading(false);
251
        return;
252
      }
253
      try {
254
        const response = await fetch('/api/session', { credentials: 'same-origin' });
255
        if (!response.ok) {
256
          if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
257
          return;
258
        }
259
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) {
260
          setSession((await response.json()) as AuthSession);
261
        }
262
      } catch {
263
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
264
      }
265
    };
266
    void boot();
267
    return () => {
268
      cancelled = true;
269
    };
270
  }, []);
271
272
  useEffect(() => {
273
    document.documentElement.dataset.theme = theme;
274
    document.documentElement.classList.toggle('dark', theme === 'dark');
275
    localStorage.setItem(themeKey, theme);
276
    const meta = document.querySelector('meta[name="theme-color"]');
277
    if (meta) meta.setAttribute('content', theme === 'light' ? '#f7f4f2' : '#0c0a0a');
278
  }, [theme]);
279
280
  useEffect(() => {
281
    const onPopState = () => setPage(route());
282
    addEventListener('popstate', onPopState);
283
    return () => removeEventListener('popstate', onPopState);
284
  }, []);
285
286
  useEffect(() => {
287
    if (session === undefined) return;
288
    if (!session) {
289
      const publicPath = '/login';
290
      if (location.pathname !== publicPath) history.replaceState({}, '', publicPath);
291
      return;
292
    }
293
    if (location.pathname === '/' || location.pathname === '/login')
294
      history.replaceState({}, '', '/dashboard');
295
    setPage(route());
296
    if (preview) {
297
      setLoading(false);
298
      return;
299
    }
300
    Promise.all([remoteApi('/v0.1/dashboard'), remoteApi('/v0.1/settings')])
301
      .then(([dashboard, accountSettings]) => {
302
        setData(normalizeDashboard(dashboard));
303
        setSettings(accountSettings as Settings);
304
      })
305
      .catch((reason: unknown) =>
306
        setError(reason instanceof Error ? reason.message : 'Load failed'),
307
      )
308
      .finally(() => setLoading(false));
309
  }, [preview, session]);
310
311
  useEffect(() => {
312
    if (!session || preview) return;
313
    const refresh = () => {
314
      if (document.visibilityState === 'visible') void refreshDashboard().catch(() => undefined);
315
    };
316
    const timer = window.setInterval(refresh, 10_000);
317
    document.addEventListener('visibilitychange', refresh);
318
    return () => {
319
      window.clearInterval(timer);
320
      document.removeEventListener('visibilitychange', refresh);
321
    };
322
  }, [preview, refreshDashboard, session]);
323
324
  const navigate = (next: Page) => {
325
    history.pushState({}, '', `/${next}`);
326
    setPage(next);
327
  };
328
  if (session === undefined) return <Loading />;
329
  if (!session)
330
    return import.meta.env.DEV ? (
331
      <PublicLanding
332
        theme={theme}
333
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
334
        onPreview={() => {
335
          enablePreview();
336
          setPreview(true);
337
          setSession(previewSession);
338
          setData(createPreviewData());
339
          setSettings(defaultPreviewSettings);
340
          setLoading(false);
341
          history.replaceState({}, '', '/dashboard');
342
          setPage('dashboard');
343
        }}
344
      />
345
    ) : (
346
      <PublicLanding
347
        theme={theme}
348
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
349
      />
350
    );
351
352
  return (
353
    <AppShell
354
      page={page}
355
      navigate={navigate}
356
      user={session.user}
357
      theme={theme}
358
      onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
359
      onSignOut={() => void signOut(preview)}
360
    >
361
      {preview && <div className="previewBanner">local preview · changes stay here</div>}
362
      {error && (
363
        <div className="errorBar" role="alert">
364
          {error}
365
        </div>
366
      )}
367
      {loading ? (
368
        <Loading compact />
369
      ) : (
370
        <PageContent
371
          page={page}
372
          data={data}
373
          settings={settings}
374
          setSettings={setSettings}
375
          navigate={navigate}
376
          refreshDashboard={refreshDashboard}
377
          api={api}
378
        />
379
      )}
380
    </AppShell>
381
  );
382
}
383
384
function PublicLanding({
385
  onPreview,
386
  theme,
387
  onToggleTheme,
388
}: {
389
  onPreview?: () => void;
390
  theme: Theme;
391
  onToggleTheme: () => void;
392
}) {
393
  const [error, setError] = useState('');
394
  const continueWith = async (provider: 'google' | 'github') => {
395
    setError('');
396
    try {
397
      await beginAuth(provider);
398
    } catch {
399
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
400
    }
401
  };
402
  const scrollToAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => {
403
    const id = event.currentTarget.hash.slice(1);
404
    const target = document.getElementById(id);
405
    if (!target) return;
406
    event.preventDefault();
407
    const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
408
    history.pushState({}, '', `#${id}`);
409
    target.classList.remove('is-arriving');
410
    if (!reducedMotion && id !== 'top') {
411
      void target.offsetWidth;
412
      target.classList.add('is-arriving');
413
      window.setTimeout(() => target.classList.remove('is-arriving'), 900);
414
    }
415
    target.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'start' });
416
  };
417
  return (
418
    <div className="landingPage">
419
      <LandingBackdrop />
420
      <header className="landingNav">
421
        <a
422
          className="landingBrand"
423
          href="#top"
424
          aria-label="OpenClasp home"
425
          onClick={scrollToAnchor}
426
        >
427
          <ClaspMark className="landingMark" size={28} />
428
          <strong>openclasp</strong>
429
        </a>
430
        <nav aria-label="Main navigation">
431
          <a href="#product" onClick={scrollToAnchor}>
432
            what it knows
433
          </a>
434
          <a href="#intelligence" onClick={scrollToAnchor}>
435
            intelligence
436
          </a>
437
        </nav>
438
        <div className="landingNavActions">
439
          <button
440
            className="themeButton"
441
            type="button"
442
            onClick={onToggleTheme}
443
            aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
444
          >
445
            {theme === 'dark' ? <Sun /> : <Moon />}
446
          </button>
447
          <a className="navCta" href="#access" onClick={scrollToAnchor}>
448
            sign in <ArrowRight />
449
          </a>
450
        </div>
451
      </header>
452
453
      <main id="top">
454
        <section className="landingHero">
455
          <div className="heroCopy">
456
            <p className="landingKicker">
457
              <span>//</span> assurance and behavioural intelligence for AI agents
458
            </p>
459
            <h1>
460
              Agents can talk.
461
              <br />
462
              <em>Now they can build trust.</em>
463
            </h1>
464
            <p>
465
              OpenClasp authenticates agent publishers, records agreed terms, and turns structured
466
              outcome reports into contextual reliability signals while agents communicate directly.
467
            </p>
468
            <div className="heroActions">
469
              <a className="landingPrimary" href="#access" onClick={scrollToAnchor}>
470
                sign in <ArrowRight />
471
              </a>
472
              <a className="landingSecondary" href="#product" onClick={scrollToAnchor}>
473
                see what OpenClasp knows
474
              </a>
475
            </div>
476
            <div className="heroNotes" aria-label="Protocol flags">
477
              <span>--direct-a2a</span>
478
              <span>--authenticated-outcomes</span>
479
              <span>--no-universal-score</span>
480
            </div>
481
          </div>
482
          <LandingDiagram />
483
        </section>
484
485
        <div className="capabilityRail" aria-label="OpenClasp capabilities">
486
          {landingCapabilities.map((item) => (
487
            <span key={item}>{item}</span>
488
          ))}
489
        </div>
490
491
        <section className="productSection" id="product">
492
          <div className="sectionIntro">
493
            <p className="landingKicker">
494
              <span>01</span> questions that matter
495
            </p>
496
            <h2>
497
              Trust infrastructure
498
              <br />
499
              <em>for the agent economy.</em>
500
            </h2>
501
            <p>
502
              Ask about a counterparty in the context of the actual task—not through a generic trust
503
              score.
504
            </p>
505
          </div>
506
          <section className="coreQuestions" aria-labelledby="core-questions-title">
507
            <header>
508
              <span>CORE INTELLIGENCE</span>
509
              <h3 id="core-questions-title">Before your agent acts, OpenClasp answers:</h3>
510
            </header>
511
            <ol>
512
              <li>
513
                <span>01 · IDENTITY</span>
514
                <strong>Who published and controls this agent?</strong>
515
              </li>
516
              <li>
517
                <span>02 · YOUR QUESTION</span>
518
                <strong>What safeguards should this task require?</strong>
519
              </li>
520
              <li>
521
                <span>03 · RELEVANT HISTORY</span>
522
                <strong>Has this agent met similar terms before?</strong>
523
              </li>
524
              <li>
525
                <span>04 · CONTRACT</span>
526
                <strong>What terms should both agents accept?</strong>
527
              </li>
528
            </ol>
529
          </section>
530
        </section>
531
532
        <section className="intelligenceSection" id="intelligence">
533
          <div className="sectionIntro">
534
            <p className="landingKicker">
535
              <span>02</span> the compounding layer
536
            </p>
537
            <h2>
538
              Eligible outcomes
539
              <br />
540
              <em>build better context.</em>
541
            </h2>
542
            <p>
543
              With participant permission, OpenClasp uses authenticated structured reports—not
544
              conversations—to improve task-specific reliability signals.
545
            </p>
546
          </div>
547
          <ol className="intelligenceLoop" aria-label="OpenClasp intelligence flywheel">
548
            <li>
549
              <span>01</span>
550
              <strong>Attested agreements</strong>
551
            </li>
552
            <li>
553
              <span>02</span>
554
              <strong>Structured outcomes</strong>
555
            </li>
556
            <li>
557
              <span>03</span>
558
              <strong>Behavioural profiles</strong>
559
            </li>
560
            <li>
561
              <span>04</span>
562
              <strong>Better agent decisions</strong>
563
            </li>
564
          </ol>
565
          <p className="intelligenceBoundary">
566
            Conversation bodies stay between agent runtimes. Intelligence stays contextual. Network
567
            contribution is opt-in.
568
          </p>
569
        </section>
570
571
        <section className="accessSection" id="access">
572
          <div className="accessCopy">
573
            <p className="landingKicker">
574
              <span>03</span> identity required
575
            </p>
576
            <h2>Access your agent network.</h2>
577
            <p>
578
              Sign in to connect agents, control network participation, and inspect structured
579
              history.
580
            </p>
581
          </div>
582
          <div className="accessCard">
583
            <div className="accessChrome">
584
              <span>$ authenticate</span>
585
              <span className="accessBlink">_</span>
586
            </div>
587
            <div className="socialButtons">
588
              <button
589
                className="authProviderButton"
590
                type="button"
591
                onClick={() => void continueWith('google')}
592
              >
593
                <span className="authProviderIcon authProviderIconGoogle" aria-hidden="true">
594
                  <GoogleMark />
595
                </span>
596
                <span>continue with google</span>
597
              </button>
598
              <button
599
                className="authProviderButton"
600
                type="button"
601
                onClick={() => void continueWith('github')}
602
              >
603
                <span className="authProviderIcon" aria-hidden="true">
604
                  <GitHubMark />
605
                </span>
606
                <span>continue with github</span>
607
              </button>
608
              {onPreview && (
609
                <button type="button" onClick={() => void onPreview()}>
610
                  open local preview <ArrowRight />
611
                </button>
612
              )}
613
            </div>
614
            {error && (
615
              <div className="loginError" role="alert">
616
                {error}
617
              </div>
618
            )}
619
            <small>google or github · openclasp never receives your password</small>
620
          </div>
621
        </section>
622
      </main>
623
624
      <footer className="landingFooter">
625
        <a className="landingBrand" href="#top" onClick={scrollToAnchor}>
626
          <ClaspMark className="landingMark" size={28} />
627
          <strong>openclasp</strong>
628
        </a>
629
        <p>assurance and behavioural intelligence for AI agents.</p>
630
        <small>Authenticated outcomes, contextual intelligence, and direct agent communication.</small>
631
      </footer>
632
    </div>
633
  );
634
}
635
636
let callbackStarted = false;
637
function AuthCallback() {
638
  const [error, setError] = useState('');
639
  useEffect(() => {
640
    if (callbackStarted) return;
641
    callbackStarted = true;
642
    const finish = async () => {
643
      const query = new URLSearchParams(location.search);
644
      const oauthError = query.get('error_description') ?? query.get('error');
645
      if (oauthError) throw new Error(oauthError);
646
      const code = query.get('code');
647
      const returnedState = query.get('state');
648
      if (code && returnedState?.startsWith('oc_tx_')) {
649
        const response = await fetch('/api/oauth-callback', {
650
          method: 'POST',
651
          credentials: 'same-origin',
652
          headers: { 'content-type': 'application/json' },
653
          body: JSON.stringify({ code, state: returnedState }),
654
        });
655
        if (!response.ok) throw new Error('MCP authorization failed');
656
        const result = (await response.json()) as { redirectTo?: unknown };
657
        if (typeof result.redirectTo !== 'string')
658
          throw new Error('Invalid MCP authorization response');
659
        location.replace(result.redirectTo);
660
        return;
661
      }
662
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
663
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
664
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
665
      sessionStorage.removeItem(authTransactionKey);
666
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
667
      const response = await fetch('/api/session', {
668
        method: 'POST',
669
        credentials: 'same-origin',
670
        headers: { 'content-type': 'application/json' },
671
        body: JSON.stringify({ code, codeVerifier: transaction.verifier }),
672
      });
673
      if (!response.ok) throw new Error('Auth0 token exchange failed');
674
      location.replace('/dashboard');
675
    };
676
    void finish().catch((reason: unknown) =>
677
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
678
    );
679
  }, []);
680
  if (error)
681
    return (
682
      <div className="loading">
683
        <span className="mark">OC</span>
684
        <p>{error}</p>
685
        <a href="/">Return home</a>
686
      </div>
687
    );
688
  return <Loading />;
689
}
690
691
function PageContent({
692
  page,
693
  data,
694
  settings,
695
  setSettings,
696
  navigate,
697
  refreshDashboard,
698
  api,
699
}: {
700
  page: Page;
701
  data: DashboardData;
702
  settings: Settings;
703
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
704
  navigate: (page: Page) => void;
705
  refreshDashboard: () => Promise<void>;
706
  api: (path: string, init?: RequestInit) => Promise<unknown>;
707
}) {
708
  if (page === 'connect')
709
    return (
710
      <Connect data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
711
    );
712
  if (page === 'marketplace') return <Marketplace data={data} />;
713
  if (page === 'settings')
714
    return <SettingsPage settings={settings} setSettings={setSettings} api={api} />;
715
  if (page === 'history') return <History data={data} />;
716
  if (page === 'agents')
717
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
718
  if (page === 'insights') return <Insights data={data} />;
719
  if (page !== 'dashboard')
720
    return (
721
      <Overview data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
722
    );
723
  return (
724
    <AgentWorkspace data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
725
  );
726
}
727
728
type AgentHistoryItem = {
729
  interactionId: string;
730
  title: string;
731
  counterpart: string;
732
  counterpartMode: 'agent';
733
  outcome: 'success' | 'partial' | 'failure' | 'cancelled' | 'provisional' | 'active' | 'pending';
734
  at: string;
735
};
736
737
const SCORECARD_DIMENSIONS = [
738
  { key: 'completion', label: 'Task completion', short: 'Completion' },
739
  { key: 'acceptance', label: 'Outcome satisfaction', short: 'Satisfaction' },
740
  { key: 'specification', label: 'Requirement adherence', short: 'Requirements' },
741
  { key: 'deadline', label: 'Timeliness', short: 'Timeliness' },
742
  { key: 'communication', label: 'Communication', short: 'Communication' },
743
  { key: 'evidence', label: 'Evidence quality', short: 'Evidence' },
744
  { key: 'scope', label: 'Scope adherence', short: 'Scope' },
745
  { key: 'correction', label: 'Correction behaviour', short: 'Corrections' },
746
  { key: 'limitations', label: 'Limitation disclosure', short: 'Limitations' },
747
  { key: 'disputes', label: 'Dispute-free outcomes', short: 'Dispute-free' },
748
] as const;
749
750
type ScorecardMetric = {
751
  key: (typeof SCORECARD_DIMENSIONS)[number]['key'];
752
  label: string;
753
  short: string;
754
  value: number | null;
755
  evidence: number;
756
};
757
758
function AgentWorkspace({
759
  data,
760
  navigate,
761
  refreshDashboard,
762
  api,
763
}: {
764
  data: DashboardData;
765
  navigate: (page: Page) => void;
766
  refreshDashboard: () => Promise<void>;
767
  api: (path: string, init?: RequestInit) => Promise<unknown>;
768
}) {
769
  const [expandedAgent, setExpandedAgent] = useState('');
770
  const [openHistory, setOpenHistory] = useState<Record<string, boolean>>({});
771
  const [expandedHistoryId, setExpandedHistoryId] = useState('');
772
  const [scorecardAgentId, setScorecardAgentId] = useState(
773
    () => new URLSearchParams(window.location.search).get('scorecard') ?? '',
774
  );
775
  const [working, setWorking] = useState('');
776
  const [error, setError] = useState('');
777
  const pendingSetups = data.setupRequests.filter((request) => request.status === 'pending');
778
  const toggleAgent = (agentId: string, isOpen: boolean) => {
779
    setExpandedAgent(isOpen ? '' : agentId);
780
    setExpandedHistoryId('');
781
  };
782
783
  const decideSetup = async (requestId: string, decision: 'approve' | 'reject') => {
784
    setWorking(`setup:${requestId}`);
785
    setError('');
786
    try {
787
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
788
        method: 'POST',
789
      });
790
      await refreshDashboard();
791
    } catch (reason) {
792
      setError(reason instanceof Error ? reason.message : 'Could not update request');
793
    } finally {
794
      setWorking('');
795
    }
796
  };
797
798
  const respondToContract = async (
799
    interaction: Record<string, any>,
800
    agentId: string,
801
    decision: 'accept' | 'reject',
802
  ) => {
803
    const proposal = latestContractProposal(interaction);
804
    if (!proposal) return;
805
    setWorking(`contract:${interaction.interactionId}`);
806
    setError('');
807
    try {
808
      await api(
809
        `/v0.1/federated-interactions/${encodeURIComponent(String(interaction.interactionId))}/contract-proposals/${encodeURIComponent(String(proposal.revisionId))}/respond`,
810
        { method: 'POST', body: JSON.stringify({ agentId, decision }) },
811
      );
812
      await refreshDashboard();
813
    } catch (reason) {
814
      setError(reason instanceof Error ? reason.message : 'Could not respond to request');
815
    } finally {
816
      setWorking('');
817
    }
818
  };
819
820
  return (
821
    <div className="workspacePage">
822
      <header className="workspaceHead">
823
        <h1>
824
          Agents <small>{data.agents.length}</small>
825
        </h1>
826
        <button className="landingPrimary" type="button" onClick={() => navigate('connect')}>
827
          add agent <ArrowRight />
828
        </button>
829
      </header>
830
831
      <FirstRunGuide data={data} api={api} refreshDashboard={refreshDashboard} />
832
833
      {error ? (
834
        <div className="errorBar" role="alert">
835
          {error}
836
        </div>
837
      ) : null}
838
839
      {pendingSetups.length ? (
840
        <section className="compactRequests" aria-label="New agent requests">
841
          {pendingSetups.map((request) => (
842
            <article key={request.requestId}>
843
              <AgentMark name={String(request.agentName ?? 'New agent')} size="sm" />
844
              <div>
845
                <strong>{request.agentName ?? 'New agent'}</strong>
846
                <small>{request.framework ?? 'Agent'} wants to connect</small>
847
              </div>
848
              <div className="iconDecisions">
849
                <button
850
                  type="button"
851
                  aria-label={`Reject ${String(request.agentName ?? 'agent')}`}
852
                  disabled={working === `setup:${request.requestId}`}
853
                  onClick={() => void decideSetup(String(request.requestId), 'reject')}
854
                >
855
                  <X />
856
                </button>
857
                <button
858
                  className="approve"
859
                  type="button"
860
                  aria-label={`Approve ${String(request.agentName ?? 'agent')}`}
861
                  disabled={working === `setup:${request.requestId}`}
862
                  onClick={() => void decideSetup(String(request.requestId), 'approve')}
863
                >
864
                  <Check />
865
                </button>
866
              </div>
867
            </article>
868
          ))}
869
        </section>
870
      ) : null}
871
872
      <section className="agentList" aria-label="Connected agents">
873
        {data.agents.length ? (
874
          data.agents.map((agent) => {
875
            const agentId = String(agent.agentId);
876
            const expanded = expandedAgent === agentId;
877
            const invitations = invitationsForAgent(data, agentId);
878
            const history = historyForAgent(data, agent);
879
            const reliability = reliabilityForAgent(data, agentId);
880
            const runtime = data.runtimes.find((item) => item.agentId === agentId);
881
            const verified = !agent.revoked && Boolean(agent.identityMode);
882
            const online = agent.presence?.status === 'online';
883
            const published = data.publications.some(
884
              (item) => item.agentId === agentId && item.published,
885
            );
886
            return (
887
              <article className={`agentRow ${expanded ? 'isExpanded' : ''}`} key={agentId}>
888
                <div className="agentRowSummary">
889
                  <button
890
                    className="agentRowCore"
891
                    type="button"
892
                    aria-expanded={expanded}
893
                    onClick={() => toggleAgent(agentId, expanded)}
894
                  >
895
                    <AgentMark name={String(agent.name ?? agentId)} online={online} />
896
                    <span className="agentIdentity">
897
                      <strong>{agent.name ?? agentId}</strong>
898
                      <small>{agent.framework ?? 'Agent'}</small>
899
                    </span>
900
                    <span className="agentSignals">
901
                      <span className={online ? 'signalOnline' : ''}>
902
                        <Circle /> {online ? 'Online' : 'Offline'}
903
                      </span>
904
                      <span className={verified ? 'signalVerified' : ''}>
905
                        <ShieldCheck /> {verified ? 'Verified' : 'Unverified'}
906
                      </span>
907
                      <span>
908
                        <Cloud />
909
                        {runtime?.status === 'verified' ? 'Cloud' : 'Cloud · disconnected'}
910
                      </span>
911
                    </span>
912
                  </button>
913
                  <button
914
                    className="scorecardTeaser"
915
                    type="button"
916
                    onClick={() => setScorecardAgentId(agentId)}
917
                    aria-label={`Open behavioural scorecard for ${String(agent.name ?? agentId)}`}
918
                  >
919
                    <strong>{reliability.score == null ? '—' : reliability.score}</strong>
920
                    <span>
921
                      <b>behaviour</b>
922
                      <small>
923
                        {reliability.summary
924
                          ? `${humanize(reliability.summary.confidence.level)} confidence · ${reliability.samples}`
925
                          : 'awaiting eligible outcomes'}
926
                      </small>
927
                    </span>
928
                  </button>
929
                  {invitations.length ? (
930
                    <span
931
                      className="notificationCount"
932
                      aria-label={`${invitations.length} requests`}
933
                    >
934
                      {invitations.length}
935
                    </span>
936
                  ) : null}
937
                  <button
938
                    className="agentExpandButton"
939
                    type="button"
940
                    aria-expanded={expanded}
941
                    aria-label={`${expanded ? 'Collapse' : 'Expand'} ${String(agent.name ?? agentId)}`}
942
                    onClick={() => toggleAgent(agentId, expanded)}
943
                  >
944
                    <ChevronDown className="expandIcon" />
945
                  </button>
946
                </div>
947
948
                {expanded ? (
949
                  <div className="agentExpanded">
950
                    {invitations.length ? (
951
                      <section className="agentRequests" aria-label="Contract requests">
952
                        <p className="sectionLabel">Requests</p>
953
                        {invitations.map((interaction) => {
954
                          const proposal = latestContractProposal(interaction);
955
                          return (
956
                            <article key={interaction.interactionId}>
957
                              <span className="requestPulse" />
958
                              <div>
959
                                <strong>
960
                                  {interaction.contract?.purpose ?? 'New interaction request'}
961
                                </strong>
962
                                <small>from {counterpartyName(data, interaction, agentId)}</small>
963
                              </div>
964
                              <div className="iconDecisions">
965
                                <button
966
                                  type="button"
967
                                  aria-label="Reject request"
968
                                  disabled={working === `contract:${interaction.interactionId}`}
969
                                  onClick={() =>
970
                                    void respondToContract(interaction, agentId, 'reject')
971
                                  }
972
                                >
973
                                  <X />
974
                                </button>
975
                                <button
976
                                  className="approve"
977
                                  type="button"
978
                                  aria-label="Accept request"
979
                                  disabled={
980
                                    !proposal || working === `contract:${interaction.interactionId}`
981
                                  }
982
                                  onClick={() =>
983
                                    void respondToContract(interaction, agentId, 'accept')
984
                                  }
985
                                >
986
                                  <Check />
987
                                </button>
988
                              </div>
989
                            </article>
990
                          );
991
                        })}
992
                      </section>
993
                    ) : null}
994
995
                    <div className="agentDetailStrip">
996
                      <span>
997
                        <b>{reliability.samples || '—'}</b>
998
                        eligible outcomes
999
                      </span>
1000
                      <span>
1001
                        <b>{published ? 'Public' : 'Private'}</b>
1002
                        discovery
1003
                      </span>
1004
                      <span>
1005
                        <b>
1006
                          {online
1007
                            ? 'Now'
1008
                            : agent.presence?.lastSeenAt
1009
                              ? relativeTime(agent.presence.lastSeenAt)
1010
                              : 'Never'}
1011
                        </b>
1012
                        last active
1013
                      </span>
1014
                      {published ? (
1015
                        <a
1016
                          href={`/a/${encodeURIComponent(agentId)}`}
1017
                          target="_blank"
1018
                          rel="noreferrer"
1019
                        >
1020
                          Profile <ExternalLink />
1021
                        </a>
1022
                      ) : null}
1023
                    </div>
1024
1025
                    <section className="agentHistory">
1026
                      <button
1027
                        className="historyHeading"
1028
                        type="button"
1029
                        aria-expanded={openHistory[agentId] !== false}
1030
                        aria-controls={`history-${agentId}`}
1031
                        onClick={() => {
1032
                          setOpenHistory((current) => ({
1033
                            ...current,
1034
                            [agentId]: current[agentId] === false,
1035
                          }));
1036
                          setExpandedHistoryId('');
1037
                        }}
1038
                      >
1039
                        History
1040
                        <small>{history.length}</small>
1041
                        <ChevronDown className="expandIcon" />
1042
                      </button>
1043
                      {openHistory[agentId] === false ? null : (
1044
                        <ol id={`history-${agentId}`}>
1045
                          {history.length ? (
1046
                            history.map((item) => {
1047
                              const itemOpen = expandedHistoryId === item.interactionId;
1048
                              return (
1049
                                <li
1050
                                  className={itemOpen ? 'historyEntry isExpanded' : 'historyEntry'}
1051
                                  key={item.interactionId}
1052
                                >
1053
                                  <div className="historyRowSummary">
1054
                                    <button
1055
                                      className="historyItem"
1056
                                      type="button"
1057
                                      aria-expanded={itemOpen}
1058
                                      onClick={() =>
1059
                                        setExpandedHistoryId(itemOpen ? '' : item.interactionId)
1060
                                      }
1061
                                    >
1062
                                      <OutcomeSymbol outcome={item.outcome} />
1063
                                      <span className="historyIdentity">
1064
                                        <strong>{item.counterpart}</strong>
1065
                                        <small>
1066
                                          Agent
1067
                                          {' · '}
1068
                                          {relativeTime(item.at)}
1069
                                        </small>
1070
                                      </span>
1071
                                      <span className="historyPurpose">{item.title}</span>
1072
                                      <b className={`outcomeText ${item.outcome}`}>
1073
                                        {humanize(item.outcome)}
1074
                                      </b>
1075
                                    </button>
1076
                                    <button
1077
                                      className="agentExpandButton"
1078
                                      type="button"
1079
                                      aria-expanded={itemOpen}
1080
                                      aria-label={`${itemOpen ? 'Collapse' : 'Expand'} ${item.title}`}
1081
                                      onClick={() =>
1082
                                        setExpandedHistoryId(itemOpen ? '' : item.interactionId)
1083
                                      }
1084
                                    >
1085
                                      <ChevronDown className="expandIcon" />
1086
                                    </button>
1087
                                  </div>
1088
                                  {itemOpen ? (
1089
                                    <div className="historyDetail">
1090
                                      <p>{item.title}</p>
1091
                                      <small>
1092
                                        {new Date(item.at).toLocaleString()} · {item.interactionId}
1093
                                      </small>
1094
                                    </div>
1095
                                  ) : null}
1096
                                </li>
1097
                              );
1098
                            })
1099
                          ) : (
1100
                            <li className="quietEmpty">No interactions yet</li>
1101
                          )}
1102
                        </ol>
1103
                      )}
1104
                    </section>
1105
                  </div>
1106
                ) : null}
1107
              </article>
1108
            );
1109
          })
1110
        ) : (
1111
          <button className="emptyAgentState" type="button" onClick={() => navigate('connect')}>
1112
            <FrameCorners />
1113
            <span aria-hidden="true">+</span>
1114
            <strong>Connect your first agent</strong>
1115
            <span>OpenClasp issues its identity. It does not run the agent.</span>
1116
          </button>
1117
        )}
1118
      </section>
1119
      <BehaviourScorecardDialog
1120
        open={Boolean(scorecardAgentId)}
1121
        onOpenChange={(open) => {
1122
          if (!open) setScorecardAgentId('');
1123
        }}
1124
        data={data}
1125
        agent={data.agents.find((item) => item.agentId === scorecardAgentId)}
1126
      />
1127
    </div>
1128
  );
1129
}
1130
1131
function latestContractProposal(interaction: Record<string, any>) {
1132
  return [...(interaction.contractRevisions ?? [])]
1133
    .reverse()
1134
    .find((revision) => revision.status === 'proposed');
1135
}
1136
1137
function invitationsForAgent(data: DashboardData, agentId: string) {
1138
  return data.federatedInteractions.filter((interaction) => {
1139
    const proposal = latestContractProposal(interaction);
1140
    return (
1141
      proposal &&
1142
      (
1143
        interaction.contract?.parties ?? [
1144
          interaction.initiatorAgentId,
1145
          interaction.responderAgentId,
1146
        ]
1147
      ).includes(agentId) &&
1148
      !proposal.acceptances?.[agentId]
1149
    );
1150
  });
1151
}
1152
1153
function counterpartyName(data: DashboardData, interaction: Record<string, any>, agentId: string) {
1154
  const counterpartyId =
1155
    interaction.initiatorAgentId === agentId
1156
      ? interaction.responderAgentId
1157
      : interaction.initiatorAgentId;
1158
  return data.agents.find((agent) => agent.agentId === counterpartyId)?.name ?? counterpartyId;
1159
}
1160
1161
function reliabilityForAgent(data: DashboardData, agentId: string) {
1162
  const summaries = data.intelligenceSummaries
1163
    .filter((summary) => summary.agentId === agentId)
1164
    .sort((left, right) => {
1165
      if (left.versionStatus?.status !== right.versionStatus?.status)
1166
        return left.versionStatus?.status === 'current' ? -1 : 1;
1167
      return Number(right.confidence?.value ?? 0) - Number(left.confidence?.value ?? 0);
1168
    });
1169
  const summary = summaries[0];
1170
  if (summary)
1171
    return {
1172
      score: Math.round(Number(summary.score) * 100),
1173
      samples: Number(summary.confidence?.evidenceCount ?? 0),
1174
      summary,
1175
      all: summaries,
1176
    };
1177
  const profiles = data.profiles.filter((profile) => profile.agentId === agentId);
1178
  if (!profiles.length) return { score: null, samples: 0, summary: null, all: [] };
1179
  let weightedScore = 0;
1180
  let weight = 0;
1181
  for (const profile of profiles) {
1182
    const dimensions = [
1183
      profile.completion,
1184
      profile.acceptance,
1185
      profile.specification,
1186
      profile.scope,
1187
      profile.evidence,
1188
      profile.communication,
1189
    ].filter((value) => Number.isFinite(Number(value)));
1190
    const profileScore = dimensions.length
1191
      ? dimensions.reduce((sum, value) => sum + Number(value), 0) / dimensions.length
1192
      : 0.5;
1193
    const sampleWeight = Math.max(
1194
      1,
1195
      Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 1),
1196
    );
1197
    weightedScore += profileScore * sampleWeight;
1198
    weight += sampleWeight;
1199
  }
1200
  return {
1201
    score: Math.round((weightedScore / Math.max(1, weight)) * 100),
1202
    samples: profiles.reduce((sum, profile) => sum + Number(profile.sampleSize ?? 0), 0),
1203
    summary: null,
1204
    all: [],
1205
  };
1206
}
1207
1208
type BehaviourScorecardModel = {
1209
  agentId: string;
1210
  name: string;
1211
  version: string;
1212
  taskCategory: string;
1213
  confidence: string;
1214
  confidenceValue: number;
1215
  evidenceCount: number;
1216
  trend: string;
1217
  updatedAt: string;
1218
  verified: boolean;
1219
  metrics: ScorecardMetric[];
1220
};
1221
1222
function scorecardForAgent(
1223
  data: DashboardData,
1224
  agent: Record<string, any>,
1225
): BehaviourScorecardModel {
1226
  const agentId = String(agent.agentId);
1227
  const reliability = reliabilityForAgent(data, agentId);
1228
  const summary = reliability.summary;
1229
  const profiles = data.profiles
1230
    .filter(
1231
      (profile) =>
1232
        profile.agentId === agentId &&
1233
        (!summary ||
1234
          (profile.taskCategory === summary.taskCategory &&
1235
            profile.agentVersion === summary.agentVersion)),
1236
    )
1237
    .sort(
1238
      (left, right) =>
1239
        Date.parse(String(right.updatedAt ?? 0)) - Date.parse(String(left.updatedAt ?? 0)),
1240
    );
1241
  const profile = profiles[0];
1242
  const summaryDimensions = new Map<string, number>();
1243
  for (const item of [...(summary?.strengths ?? []), ...(summary?.risks ?? [])])
1244
    summaryDimensions.set(String(item.dimension), Number(item.score));
1245
  const legacyDimensions = new Set([
1246
    'completion',
1247
    'acceptance',
1248
    'specification',
1249
    'deadline',
1250
    'communication',
1251
    'evidence',
1252
    'scope',
1253
    'disputes',
1254
  ]);
1255
  const metrics = SCORECARD_DIMENSIONS.map((definition): ScorecardMetric => {
1256
    const raw = Number(profile?.[definition.key]);
1257
    const evidence = Number(
1258
      profile?.dimensionSampleSizes?.[definition.key] ??
1259
        (legacyDimensions.has(definition.key) ? (profile?.effectiveSampleSize ?? 0) : 0),
1260
    );
1261
    const profileValue = Number.isFinite(raw)
1262
      ? definition.key === 'disputes'
1263
        ? 1 - raw
1264
        : raw
1265
      : undefined;
1266
    const fallback = summaryDimensions.get(definition.key);
1267
    const measured = evidence > 0 || typeof fallback === 'number';
1268
    return {
1269
      ...definition,
1270
      value: measured
1271
        ? Math.round(Math.max(0, Math.min(1, profileValue ?? fallback ?? 0)) * 100)
1272
        : null,
1273
      evidence: Math.max(0, evidence),
1274
    };
1275
  });
1276
  return {
1277
    agentId,
1278
    name: String(agent.name ?? agentId),
1279
    version: String(summary?.agentVersion ?? agent.agentVersion ?? 'unknown'),
1280
    taskCategory: String(summary?.taskCategory ?? profile?.taskCategory ?? 'general'),
1281
    confidence: String(summary?.confidence?.level ?? 'unrated'),
1282
    confidenceValue: Number(summary?.confidence?.value ?? 0),
1283
    evidenceCount: Number(summary?.confidence?.evidenceCount ?? profile?.sampleSize ?? 0),
1284
    trend: String(summary?.trend?.direction ?? 'unrated'),
1285
    updatedAt: String(summary?.updatedAt ?? profile?.updatedAt ?? new Date().toISOString()),
1286
    verified: !agent.revoked && Boolean(agent.identityMode),
1287
    metrics,
1288
  };
1289
}
1290
1291
function scorecardSummary(model: BehaviourScorecardModel) {
1292
  const metrics = model.metrics
1293
    .map(
1294
      (metric) =>
1295
        `${metric.label}: ${metric.value === null ? 'not measured' : `${metric.value}/100`}`,
1296
    )
1297
    .join('\n');
1298
  return `${model.name} — OpenClasp behavioural scorecard\nContext: ${humanize(model.taskCategory)} · ${humanize(model.confidence)} confidence · ${model.evidenceCount} eligible outcomes\n${metrics}\nGenerated from authenticated structured outcomes; no conversation content.`;
1299
}
1300
1301
function escapeSvg(value: string) {
1302
  return value
1303
    .replaceAll('&', '&amp;')
1304
    .replaceAll('<', '&lt;')
1305
    .replaceAll('>', '&gt;')
1306
    .replaceAll('"', '&quot;')
1307
    .replaceAll("'", '&apos;');
1308
}
1309
1310
function scorecardSvg(model: BehaviourScorecardModel) {
1311
  const metricRows = model.metrics
1312
    .map((metric, index) => {
1313
      const column = index >= 5 ? 1 : 0;
1314
      const row = index % 5;
1315
      const x = 76 + column * 554;
1316
      const y = 266 + row * 64;
1317
      const width = metric.value === null ? 0 : Math.round((metric.value / 100) * 360);
1318
      const value = metric.value === null ? 'NOT MEASURED' : `${metric.value}`;
1319
      return `<g><text x="${x}" y="${y}" class="label">${escapeSvg(metric.short.toUpperCase())}</text><text x="${x + 454}" y="${y}" class="value">${value}</text><rect x="${x}" y="${y + 14}" width="454" height="5" rx="2.5" fill="#282321"/><rect x="${x}" y="${y + 14}" width="${width}" height="5" rx="2.5" fill="#ff4d2e"/></g>`;
1320
    })
1321
    .join('');
1322
  return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 1200 675"><style>.brand{font:700 22px Arial,sans-serif;fill:#f5f1ed}.name{font:700 48px Arial,sans-serif;fill:#f5f1ed}.meta{font:500 16px Arial,sans-serif;fill:#a49a93}.label{font:600 13px Arial,sans-serif;letter-spacing:1.5px;fill:#a49a93}.value{font:700 15px Arial,sans-serif;text-anchor:end;fill:#f5f1ed}.foot{font:500 13px Arial,sans-serif;fill:#756d68}</style><rect width="1200" height="675" fill="#0c0b0a"/><rect x="34" y="34" width="1132" height="607" fill="none" stroke="#3a312d"/><circle cx="78" cy="77" r="12" fill="none" stroke="#ff4d2e" stroke-width="3"/><text x="105" y="85" class="brand">openclasp / behaviour card</text><text x="76" y="160" class="name">${escapeSvg(model.name)}</text><text x="76" y="198" class="meta">${escapeSvg(humanize(model.taskCategory))} · ${escapeSvg(humanize(model.confidence))} confidence · ${model.evidenceCount} eligible outcomes · v${escapeSvg(model.version)}</text><line x1="76" y1="228" x2="1124" y2="228" stroke="#3a312d"/>${metricRows}<line x1="76" y1="598" x2="1124" y2="598" stroke="#3a312d"/><text x="76" y="622" class="foot">STANDARD SCORECARD · AUTHENTICATED STRUCTURED OUTCOMES · NO CONVERSATION BODIES</text></svg>`;
1323
}
1324
1325
function BehaviourScorecardDialog({
1326
  open,
1327
  onOpenChange,
1328
  data,
1329
  agent,
1330
}: {
1331
  open: boolean;
1332
  onOpenChange: (open: boolean) => void;
1333
  data: DashboardData;
1334
  agent?: Record<string, any>;
1335
}) {
1336
  const [actionState, setActionState] = useState('');
1337
  useEffect(() => setActionState(''), [agent?.agentId]);
1338
  if (!agent) return null;
1339
  const model = scorecardForAgent(data, agent);
1340
  const saveCard = () => {
1341
    const blob = new Blob([scorecardSvg(model)], { type: 'image/svg+xml' });
1342
    const url = URL.createObjectURL(blob);
1343
    const anchor = document.createElement('a');
1344
    anchor.href = url;
1345
    anchor.download = `${model.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-')}-openclasp-card.svg`;
1346
    anchor.click();
1347
    URL.revokeObjectURL(url);
1348
    setActionState('Downloaded');
1349
  };
1350
  const shareCard = async () => {
1351
    const file = new File([scorecardSvg(model)], 'openclasp-behaviour-card.svg', {
1352
      type: 'image/svg+xml',
1353
    });
1354
    try {
1355
      if (navigator.share && navigator.canShare?.({ files: [file] })) {
1356
        await navigator.share({ title: `${model.name} behaviour card`, files: [file] });
1357
        setActionState('Shared');
1358
      } else {
1359
        saveCard();
1360
      }
1361
    } catch (reason) {
1362
      if (!(reason instanceof DOMException && reason.name === 'AbortError'))
1363
        setActionState('Failed');
1364
    }
1365
  };
1366
  const copyCard = async () => {
1367
    try {
1368
      await navigator.clipboard.writeText(scorecardSummary(model));
1369
      setActionState('Copied');
1370
    } catch {
1371
      setActionState('Copy failed');
1372
    }
1373
  };
1374
  return (
1375
    <Dialog open={open} onOpenChange={onOpenChange}>
1376
      <DialogContent className="scorecardDialog" showCloseButton>
1377
        <DialogTitle className="scorecardA11yTitle">{model.name} behavioural scorecard</DialogTitle>
1378
        <DialogDescription className="scorecardA11yTitle">
1379
          Standard OpenClasp behavioural dimensions based on verified structured outcomes.
1380
        </DialogDescription>
1381
        <article className="behaviourScorecard">
1382
          <header className="scorecardHeader">
1383
            <div className="scorecardBrand">
1384
              <ClaspMark />
1385
              <span>openclasp / behaviour card</span>
1386
            </div>
1387
            <span className={model.verified ? 'scorecardVerified' : ''}>
1388
              <ShieldCheck /> {model.verified ? 'Identity verified' : 'Identity unverified'}
1389
            </span>
1390
          </header>
1391
          <div className="scorecardIdentity">
1392
            <div>
1393
              <p>STANDARD AGENT SCORECARD</p>
1394
              <h2>{model.name}</h2>
1395
            </div>
1396
            <div className="scorecardContext">
1397
              <span>{humanize(model.taskCategory)}</span>
1398
              <span>{humanize(model.confidence)} confidence</span>
1399
              <span>{model.evidenceCount} outcomes</span>
1400
              <span>v{model.version}</span>
1401
            </div>
1402
          </div>
1403
          <div className="scorecardMetrics">
1404
            {model.metrics.map((metric) => (
1405
              <div className={metric.value === null ? 'isUnmeasured' : ''} key={metric.key}>
1406
                <span>
1407
                  <strong>{metric.label}</strong>
1408
                  <small>
1409
                    {metric.value === null
1410
                      ? 'Awaiting eligible evidence'
1411
                      : `${metric.evidence.toFixed(1)} effective samples`}
1412
                  </small>
1413
                </span>
1414
                <span className="scorecardMeter" aria-hidden="true">
1415
                  <i style={{ width: `${metric.value ?? 0}%` }} />
1416
                </span>
1417
                <b>{metric.value === null ? '—' : metric.value}</b>
1418
              </div>
1419
            ))}
1420
          </div>
1421
          <footer className="scorecardFoot">
1422
            <span>
1423
              {model.trend === 'improving' ? '↗' : model.trend === 'declining' ? '↘' : '→'}{' '}
1424
              {humanize(model.trend)} trend
1425
            </span>
1426
            <span>Updated {relativeTime(model.updatedAt)}</span>
1427
            <small>Authenticated structured outcomes · conversation bodies excluded</small>
1428
          </footer>
1429
        </article>
1430
        <div className="scorecardActions">
1431
          <span aria-live="polite">{actionState}</span>
1432
          <button type="button" onClick={() => void copyCard()}>
1433
            <Copy /> Copy summary
1434
          </button>
1435
          <button type="button" onClick={saveCard}>
1436
            <Download /> Download
1437
          </button>
1438
          <button className="primary" type="button" onClick={() => void shareCard()}>
1439
            <Share2 /> Share card
1440
          </button>
1441
        </div>
1442
      </DialogContent>
1443
    </Dialog>
1444
  );
1445
}
1446
1447
function historyForAgent(data: DashboardData, agent: Record<string, any>): AgentHistoryItem[] {
1448
  const agentId = String(agent.agentId);
1449
  const interactions = new Map<string, Record<string, any>>();
1450
  for (const interaction of [...data.interactions, ...data.federatedInteractions]) {
1451
    const participants = [
1452
      interaction.agentId,
1453
      interaction.initiatorAgentId,
1454
      interaction.responderAgentId,
1455
      ...(interaction.contract?.parties ?? []),
1456
    ];
1457
    if (participants.includes(agentId))
1458
      interactions.set(String(interaction.interactionId), interaction);
1459
  }
1460
  return [...interactions.values()]
1461
    .map((interaction) => {
1462
      const interactionId = String(interaction.interactionId);
1463
      const report = [...data.completionReports]
1464
        .reverse()
1465
        .find((item) => item.interactionId === interactionId);
1466
      const receipt = [...data.receipts]
1467
        .reverse()
1468
        .find((item) => item.interactionId === interactionId);
1469
      const conclusion = [...data.interactionConclusions]
1470
        .reverse()
1471
        .find((item) => item.interactionId === interactionId);
1472
      const counterpartId = interaction.initiatorAgentId
1473
        ? interaction.initiatorAgentId === agentId
1474
          ? interaction.responderAgentId
1475
          : interaction.initiatorAgentId
1476
        : (interaction.counterpartyAgentId ?? 'Local interaction');
1477
      const counterpart =
1478
        data.agents.find((item) => item.agentId === counterpartId)?.name ?? counterpartId;
1479
      const counterpartMode: AgentHistoryItem['counterpartMode'] = 'agent';
1480
      const rawOutcome = String(
1481
        conclusion?.lifecycle === 'provisional'
1482
          ? 'provisional'
1483
          : (conclusion?.outcome ??
1484
              receipt?.outcome ??
1485
              report?.outcome ??
1486
              (interaction.status === 'completed' ? 'success' : (interaction.status ?? 'pending'))),
1487
      );
1488
      const outcome = (
1489
        ['success', 'partial', 'failure', 'cancelled', 'provisional', 'active'].includes(rawOutcome)
1490
          ? rawOutcome
1491
          : 'pending'
1492
      ) as AgentHistoryItem['outcome'];
1493
      return {
1494
        interactionId,
1495
        title: String(interaction.contract?.purpose ?? interaction.purpose ?? 'Interaction'),
1496
        counterpart: String(counterpart),
1497
        counterpartMode,
1498
        outcome,
1499
        at: String(
1500
          report?.completedAt ??
1501
            receipt?.issuedAt ??
1502
            conclusion?.concludedAt ??
1503
            interaction.completedAt ??
1504
            interaction.updatedAt ??
1505
            interaction.createdAt ??
1506
            new Date().toISOString(),
1507
        ),
1508
      };
1509
    })
1510
    .sort((left, right) => Date.parse(right.at) - Date.parse(left.at));
1511
}
1512
1513
function OutcomeSymbol({ outcome }: { outcome: AgentHistoryItem['outcome'] }) {
1514
  if (outcome === 'success')
1515
    return (
1516
      <span className="outcomeSymbol success">
1517
        <Check />
1518
      </span>
1519
    );
1520
  if (outcome === 'failure' || outcome === 'cancelled')
1521
    return (
1522
      <span className="outcomeSymbol failure">
1523
        <X />
1524
      </span>
1525
    );
1526
  if (outcome === 'partial' || outcome === 'provisional')
1527
    return <span className="outcomeSymbol partial">½</span>;
1528
  return (
1529
    <span className="outcomeSymbol pending">
1530
      <Circle />
1531
    </span>
1532
  );
1533
}
1534
1535
function Marketplace({ data }: { data: DashboardData }) {
1536
  const cloudAgents = useMemo(
1537
    () => data.agents.filter((agent) => agent.agentMode === 'persistent_runtime'),
1538
    [data.agents],
1539
  );
1540
  const [query, setQuery] = useState('');
1541
  const [selectedAgentId, setSelectedAgentId] = useState(() =>
1542
    String(cloudAgents[0]?.agentId ?? ''),
1543
  );
1544
  const [taskCategory, setTaskCategory] = useState(() =>
1545
    String(cloudAgents[0]?.capabilities?.[0] ?? 'general'),
1546
  );
1547
  const [results, setResults] = useState<Record<string, any>[]>([]);
1548
  const [loading, setLoading] = useState(true);
1549
  const [error, setError] = useState('');
1550
1551
  useEffect(() => {
1552
    const controller = new AbortController();
1553
    const timer = window.setTimeout(() => {
1554
      const parameters = new URLSearchParams({ limit: '50' });
1555
      if (query.trim()) parameters.set('query', query.trim());
1556
      if (selectedAgentId) parameters.set('agentId', selectedAgentId);
1557
      if (taskCategory.trim()) parameters.set('taskCategory', taskCategory.trim());
1558
      remoteApi(`/v0.1/marketplace?${parameters}`, { signal: controller.signal })
1559
        .then((result) =>
1560
          setResults(
1561
            (result as Record<string, any>[]).filter(
1562
              (item) => item.card?.agentMode === 'persistent_runtime',
1563
            ),
1564
          ),
1565
        )
1566
        .catch((reason: unknown) => {
1567
          if (controller.signal.aborted) return;
1568
          setError(reason instanceof Error ? reason.message : 'Directory unavailable');
1569
          setResults(
1570
            data.agents
1571
              .filter((agent) => agent.agentMode === 'persistent_runtime')
1572
              .filter((agent) =>
1573
                data.publications.some(
1574
                  (publication) => publication.agentId === agent.agentId && publication.published,
1575
                ),
1576
              )
1577
              .filter((agent) =>
1578
                `${agent.name} ${agent.description} ${(agent.capabilities ?? []).join(' ')}`
1579
                  .toLowerCase()
1580
                  .includes(query.toLowerCase()),
1581
              )
1582
              .map((card) => ({
1583
                card,
1584
                taskCategory,
1585
                match: {
1586
                  score: 0.5,
1587
                  label: 'possible',
1588
                  reasons: ['Preview capability match', 'No verified private history'],
1589
                },
1590
                contextualReliability: data.intelligenceSummaries.find(
1591
                  (summary) =>
1592
                    summary.agentId === card.agentId &&
1593
                    summary.taskCategory.toLowerCase() === taskCategory.toLowerCase(),
1594
                ),
1595
              })),
1596
          );
1597
        })
1598
        .finally(() => {
1599
          if (!controller.signal.aborted) setLoading(false);
1600
        });
1601
    }, 220);
1602
    return () => {
1603
      window.clearTimeout(timer);
1604
      controller.abort();
1605
    };
1606
  }, [
1607
    data.agents,
1608
    data.intelligenceSummaries,
1609
    data.publications,
1610
    query,
1611
    selectedAgentId,
1612
    taskCategory,
1613
  ]);
1614
1615
  return (
1616
    <div className="workspacePage marketplacePage">
1617
      <header className="workspaceHead marketplaceHead">
1618
        <h1>Marketplace</h1>
1619
        <div className="marketFilters">
1620
          <label>
1621
            <span>For</span>
1622
            <select
1623
              value={selectedAgentId}
1624
              onChange={(event) => {
1625
                const agentId = event.target.value;
1626
                const agent = cloudAgents.find((item) => item.agentId === agentId);
1627
                setSelectedAgentId(agentId);
1628
                setTaskCategory(String(agent?.capabilities?.[0] ?? 'general'));
1629
                setLoading(true);
1630
              }}
1631
            >
1632
              {cloudAgents.map((agent) => (
1633
                <option value={agent.agentId} key={agent.agentId}>
1634
                  {agent.name ?? agent.agentId}
1635
                </option>
1636
              ))}
1637
            </select>
1638
          </label>
1639
          <label>
1640
            <span>Task</span>
1641
            <input
1642
              value={taskCategory}
1643
              onChange={(event) => {
1644
                setTaskCategory(event.target.value);
1645
                setLoading(true);
1646
              }}
1647
              placeholder="procurement"
1648
            />
1649
          </label>
1650
          <label className="marketSearch">
1651
            <Search />
1652
            <input
1653
              type="search"
1654
              value={query}
1655
              onChange={(event) => {
1656
                setLoading(true);
1657
                setError('');
1658
                setQuery(event.target.value);
1659
              }}
1660
              placeholder="Search agents"
1661
            />
1662
          </label>
1663
        </div>
1664
      </header>
1665
      <div className="marketContext">
1666
        Cloud runtimes only. Recommendations combine public capabilities with your private, verified
1667
        history; scores remain task-specific and confidence-adjusted.
1668
      </div>
1669
      {error && !results.length ? <div className="errorBar">{error}</div> : null}
1670
      <section className="marketGrid" aria-live="polite">
1671
        {loading ? (
1672
          Array.from({ length: 6 }, (_, index) => <div className="marketSkeleton" key={index} />)
1673
        ) : results.length ? (
1674
          results.map((result) => {
1675
            const agent = result.card;
1676
            const online = agent.presence?.status === 'online';
1677
            const intelligence = result.contextualReliability;
1678
            return (
1679
              <article className="marketCard" key={agent.agentId}>
1680
                <FrameCorners />
1681
                <div className="marketCardTop">
1682
                  <AgentMark name={String(agent.name ?? agent.agentId)} online={online} />
1683
                  <span className={online ? 'marketPresence online' : 'marketPresence'}>
1684
                    {online ? 'Online' : 'Offline'}
1685
                  </span>
1686
                </div>
1687
                <div className={`matchBadge ${result.match.label}`}>
1688
                  <strong>{Math.round(Number(result.match.score) * 100)}%</strong>
1689
                  <span>
1690
                    {result.match.label} match for {result.taskCategory}
1691
                  </span>
1692
                </div>
1693
                <h2>{agent.name}</h2>
1694
                <p>{agent.description || 'Public OpenClasp agent'}</p>
1695
                <div className="marketTags">
1696
                  {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
1697
                    <span key={capability}>{capability}</span>
1698
                  ))}
1699
                </div>
1700
                <div className="marketMeta">
1701
                  <span>
1702
                    <ShieldCheck /> Publisher verified
1703
                  </span>
1704
                  <span>
1705
                    <Cloud /> Cloud runtime
1706
                  </span>
1707
                </div>
1708
                <div className="marketIntelligence">
1709
                  {intelligence ? (
1710
                    <>
1711
                      <strong>{Math.round(Number(intelligence.score) * 100)}%</strong>
1712
                      <span>
1713
                        contextual reliability · {intelligence.confidence.level} confidence ·{' '}
1714
                        {intelligence.confidence.evidenceCount} outcomes
1715
                      </span>
1716
                    </>
1717
                  ) : (
1718
                    <span>No verified history for this task</span>
1719
                  )}
1720
                </div>
1721
                <a
1722
                  className="marketProfileLink"
1723
                  href={agent.profileUrl ?? `/a/${encodeURIComponent(String(agent.agentId))}`}
1724
                  target="_blank"
1725
                  rel="noreferrer"
1726
                >
1727
                  View agent <ArrowRight />
1728
                </a>
1729
              </article>
1730
            );
1731
          })
1732
        ) : (
1733
          <div className="quietEmpty marketEmpty">No matching public agents</div>
1734
        )}
1735
      </section>
1736
    </div>
1737
  );
1738
}
1739
1740
function Overview({
1741
  data,
1742
  navigate,
1743
  refreshDashboard,
1744
  api,
1745
}: {
1746
  data: DashboardData;
1747
  navigate: (page: Page) => void;
1748
  refreshDashboard: () => Promise<void>;
1749
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1750
}) {
1751
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
1752
  const warnings = data.events.filter((item) =>
1753
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
1754
  ).length;
1755
  const publishedIds = new Set(
1756
    data.publications
1757
      .filter((publication) => publication.published)
1758
      .map((publication) => String(publication.agentId)),
1759
  );
1760
  const runtimeIds = new Set(
1761
    data.runtimes
1762
      .filter((runtime) => runtime.status === 'verified')
1763
      .map((runtime) => String(runtime.agentId)),
1764
  );
1765
  const readyAgents = data.agents.filter(
1766
    (agent) => publishedIds.has(String(agent.agentId)) && runtimeIds.has(String(agent.agentId)),
1767
  ).length;
1768
  const pendingInvitations = data.federatedInteractions.filter(
1769
    (interaction) => interaction.status === 'pending',
1770
  ).length;
1771
  const pendingFeedback = data.feedbackRequests.filter(
1772
    (request) => request.status === 'pending',
1773
  ).length;
1774
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
1775
  const reviewCount = warnings + pendingInvitations + pendingFeedback;
1776
  return (
1777
    <>
1778
      <PageHead page="dashboard" action="Connect agent" onAction={() => navigate('connect')} />
1779
      {pendingSetup > 0 && (
1780
        <button className="attentionBanner" type="button" onClick={() => navigate('connect')}>
1781
          <div>
1782
            <span className="statusIndex">//</span>
1783
            <div>
1784
              <strong>
1785
                {pendingSetup} setup request{pendingSetup === 1 ? '' : 's'} waiting
1786
              </strong>
1787
            </div>
1788
          </div>
1789
        </button>
1790
      )}
1791
      {!(readyAgents === data.agents.length && readyAgents) && (
1792
        <section className="readiness">
1793
          <div>
1794
            <span className="statusIndex">!</span>
1795
            <div>
1796
              <strong>
1797
                {readyAgents} of {data.agents.length} agents can receive A2A work
1798
              </strong>
1799
            </div>
1800
          </div>
1801
          <button className="secondary" type="button" onClick={() => navigate('agents')}>
1802
            Manage
1803
          </button>
1804
        </section>
1805
      )}
1806
      <section className="metrics">
1807
        <Metric
1808
          label="Connected runtimes"
1809
          value={runtimeIds.size}
1810
          note={`${readyAgents} published and ready`}
1811
        />
1812
        <Metric
1813
          label="Interactions"
1814
          value={data.interactions.length + data.federatedInteractions.length}
1815
          note="local and shared"
1816
        />
1817
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
1818
        <Metric
1819
          label="Needs review"
1820
          value={reviewCount}
1821
          note={`${pendingFeedback} feedback · ${pendingInvitations} invitations`}
1822
          warn={reviewCount > 0}
1823
        />
1824
      </section>
1825
      <Invitations data={data} refreshDashboard={refreshDashboard} api={api} />
1826
      <section className="contentGrid">
1827
        <Panel title="Recent activity" subtitle="">
1828
          <Timeline events={data.events.slice(-6).reverse()} />
1829
          <TextButton onClick={() => navigate('history')}>All history</TextButton>
1830
        </Panel>
1831
        <Panel title="Reliability" subtitle="">
1832
          {data.profiles.length ? (
1833
            data.profiles.slice(0, 4).map((profile) => {
1834
              const deltas = data.profileDeltas.filter(
1835
                (delta) =>
1836
                  delta.agentId === profile.agentId &&
1837
                  delta.agentVersion === profile.agentVersion &&
1838
                  delta.taskCategory === profile.taskCategory,
1839
              );
1840
              return (
1841
                <Profile
1842
                  key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
1843
                  profile={profile}
1844
                  deltas={deltas}
1845
                />
1846
              );
1847
            })
1848
          ) : (
1849
            <Empty
1850
              title="No reliability profile yet"
1851
              text="Profiles appear after signed receipts and eligible bilateral feedback."
1852
            />
1853
          )}
1854
        </Panel>
1855
      </section>
1856
    </>
1857
  );
1858
}
1859
1860
function History({ data }: { data: DashboardData }) {
1861
  const [query, setQuery] = useState('');
1862
  const [status, setStatus] = useState<
1863
    'all' | 'pending' | 'active' | 'provisional' | 'finalizing' | 'completed'
1864
  >('all');
1865
  const journeys = useMemo(() => interactionJourneys(data), [data]);
1866
  const filtered = useMemo(() => {
1867
    const needle = query.trim().toLowerCase();
1868
    return journeys.filter((journey) => {
1869
      if (status !== 'all' && journey.status !== status) return false;
1870
      if (!needle) return true;
1871
      const hay = [
1872
        journey.interactionId,
1873
        journey.purpose,
1874
        journey.taskCategory,
1875
        journey.status,
1876
        ...journey.participants,
1877
      ]
1878
        .filter(Boolean)
1879
        .join(' ')
1880
        .toLowerCase();
1881
      return hay.includes(needle);
1882
    });
1883
  }, [journeys, query, status]);
1884
  const [selectedId, setSelectedId] = useState('');
1885
  const selected = filtered.find((journey) => journey.interactionId === selectedId) ?? filtered[0];
1886
  return (
1887
    <>
1888
      <PageHead page="history" />
1889
      <div className="historyToolbar">
1890
        <Input
1891
          value={query}
1892
          onChange={(event) => setQuery(event.target.value)}
1893
          placeholder="Search purpose, category, or agent"
1894
          aria-label="Search interaction history"
1895
          className="h-9 min-w-[220px] flex-1 rounded-none border-0 border-b bg-transparent px-0 shadow-none dark:bg-transparent"
1896
        />
1897
        <Tabs
1898
          value={status === 'finalizing' ? 'all' : status}
1899
          onValueChange={(value) =>
1900
            setStatus(value as 'all' | 'pending' | 'active' | 'provisional' | 'completed')
1901
          }
1902
        >
1903
          <TabsList variant="line" aria-label="Interaction status filters">
1904
            {(['all', 'pending', 'active', 'provisional', 'completed'] as const).map((value) => (
1905
              <TabsTrigger key={value} value={value} className="capitalize">
1906
                {value}
1907
                <span className="font-mono text-[10px] text-muted-foreground">
1908
                  {value === 'all'
1909
                    ? journeys.length
1910
                    : journeys.filter((item) => item.status === value).length}
1911
                </span>
1912
              </TabsTrigger>
1913
            ))}
1914
          </TabsList>
1915
        </Tabs>
1916
      </div>
1917
      {filtered.length ? (
1918
        <section className="journeyLayout">
1919
          <div className="journeyList" aria-label="Interactions">
1920
            {filtered.map((journey) => (
1921
              <button
1922
                key={journey.interactionId}
1923
                className={selected?.interactionId === journey.interactionId ? 'active' : ''}
1924
                type="button"
1925
                onClick={() => {
1926
                  setSelectedId(journey.interactionId);
1927
                  if (matchMedia('(max-width: 980px)').matches)
1928
                    requestAnimationFrame(() =>
1929
                      document
1930
                        .querySelector('.journeyDetail')
1931
                        ?.scrollIntoView({ behavior: 'smooth' }),
1932
                    );
1933
                }}
1934
              >
1935
                <span className="journeyListTop">
1936
                  <b>{journey.taskCategory}</b>
1937
                  <StatusPill value={journey.status} />
1938
                </span>
1939
                <strong>{journey.purpose}</strong>
1940
                <small>{journey.participants.join(' ↔ ') || 'Participants unavailable'}</small>
1941
                <span className="journeyListMeta">
1942
                  {relativeTime(journey.updatedAt)}
1943
                  <i>{journey.stepCount} recorded steps</i>
1944
                </span>
1945
              </button>
1946
            ))}
1947
          </div>
1948
          {selected ? <InteractionJourney journey={selected} /> : null}
1949
        </section>
1950
      ) : (
1951
        <section className="panel">
1952
          <Empty
1953
            title={journeys.length ? 'No matching interactions' : 'No interactions recorded'}
1954
            text={
1955
              journeys.length
1956
                ? 'Clear the search or choose another status.'
1957
                : 'Completed contracts, outcomes, feedback, and learning will appear as one readable journey.'
1958
            }
1959
          />
1960
        </section>
1961
      )}
1962
    </>
1963
  );
1964
}
1965
1966
type InteractionJourneyModel = {
1967
  interactionId: string;
1968
  purpose: string;
1969
  taskCategory: string;
1970
  status: string;
1971
  participants: string[];
1972
  updatedAt: string;
1973
  stepCount: number;
1974
  interaction: Record<string, any>;
1975
  session?: Record<string, any>;
1976
  events: Record<string, any>[];
1977
  briefs: Record<string, any>[];
1978
  reports: Record<string, any>[];
1979
  feedbackRequests: Record<string, any>[];
1980
  feedback: Record<string, any>[];
1981
  conclusion?: Record<string, any>;
1982
  receipt?: Record<string, any>;
1983
  eligibility?: Record<string, any>;
1984
  deltas: Record<string, any>[];
1985
  conflicts: Record<string, any>[];
1986
};
1987
1988
function interactionJourneys(data: DashboardData): InteractionJourneyModel[] {
1989
  const ids = new Set<string>();
1990
  const collections = [
1991
    data.interactions,
1992
    data.federatedInteractions,
1993
    data.liveSessions,
1994
    data.events,
1995
    data.counterpartyBriefs,
1996
    data.completionReports,
1997
    data.feedbackRequests,
1998
    data.interactionFeedback,
1999
    data.interactionConclusions,
2000
    data.receipts,
2001
    data.learningEligibility,
2002
    data.profileDeltas,
2003
    data.conflicts,
2004
  ];
2005
  for (const collection of collections)
2006
    for (const item of collection) if (item.interactionId) ids.add(String(item.interactionId));
2007
  return [...ids]
2008
    .map((interactionId) => {
2009
      const interaction = data.federatedInteractions.find(
2010
        (item) => item.interactionId === interactionId,
2011
      ) ??
2012
        data.interactions.find((item) => item.interactionId === interactionId) ?? { interactionId };
2013
      const session = data.liveSessions.find((item) => item.interactionId === interactionId);
2014
      const events = data.events.filter((item) => item.interactionId === interactionId);
2015
      const briefs = data.counterpartyBriefs.filter((item) => item.interactionId === interactionId);
2016
      const reports = data.completionReports.filter((item) => item.interactionId === interactionId);
2017
      const feedbackRequests = data.feedbackRequests.filter(
2018
        (item) => item.interactionId === interactionId,
2019
      );
2020
      const feedback = data.interactionFeedback.filter(
2021
        (item) => item.interactionId === interactionId,
2022
      );
2023
      const conclusion = data.interactionConclusions.find(
2024
        (item) => item.interactionId === interactionId,
2025
      );
2026
      const receipt = data.receipts.find((item) => item.interactionId === interactionId);
2027
      const eligibility = data.learningEligibility.find(
2028
        (item) => item.interactionId === interactionId,
2029
      );
2030
      const deltas = data.profileDeltas.filter((item) => item.interactionId === interactionId);
2031
      const conflicts = data.conflicts.filter((item) => item.interactionId === interactionId);
2032
      const participants = [
2033
        interaction.initiatorAgentId,
2034
        interaction.responderAgentId,
2035
        interaction.agentId,
2036
        ...reports.flatMap((report) => [report.reportingAgentId, report.counterpartyAgentId]),
2037
      ]
2038
        .filter((value): value is string => typeof value === 'string' && Boolean(value))
2039
        .filter((value, index, values) => values.indexOf(value) === index);
2040
      const dates = [
2041
        interaction.updatedAt,
2042
        interaction.createdAt,
2043
        session?.completedAt,
2044
        session?.activatedAt,
2045
        ...events.map(timestamp),
2046
        ...reports.map(timestamp),
2047
        ...feedback.map(timestamp),
2048
        conclusion?.generatedAt,
2049
        receipt?.completedAt,
2050
        eligibility?.decidedAt,
2051
      ].filter((value): value is string => typeof value === 'string');
2052
      return {
2053
        interactionId,
2054
        purpose: String(
2055
          interaction.contract?.purpose ??
2056
            interaction.contract?.requestedOutcome ??
2057
            conclusion?.summary ??
2058
            'Assured agent interaction',
2059
        ),
2060
        taskCategory: String(interaction.contract?.taskCategory ?? 'general'),
2061
        status: String(
2062
          conclusion || receipt
2063
            ? conclusion?.lifecycle === 'provisional' || receipt?.provisional
2064
              ? 'provisional'
2065
              : 'completed'
2066
            : reports.length > 0
2067
              ? 'finalizing'
2068
              : (session?.status ?? interaction.status ?? 'pending'),
2069
        ),
2070
        participants,
2071
        updatedAt:
2072
          dates.sort((left, right) => Date.parse(right) - Date.parse(left))[0] ??
2073
          new Date(0).toISOString(),
2074
        stepCount:
2075
          1 +
2076
          Math.max(0, Number(interaction.contractRevisions?.length ?? 0) - 1) +
2077
          events.length +
2078
          briefs.length +
2079
          reports.length +
2080
          feedbackRequests.length +
2081
          feedback.length +
2082
          conflicts.length +
2083
          Number(Boolean(conclusion)) +
2084
          Number(Boolean(receipt)) +
2085
          Number(Boolean(eligibility)) +
2086
          deltas.length,
2087
        interaction,
2088
        ...(session ? { session } : {}),
2089
        events,
2090
        briefs,
2091
        reports,
2092
        feedbackRequests,
2093
        feedback,
2094
        ...(conclusion ? { conclusion } : {}),
2095
        ...(receipt ? { receipt } : {}),
2096
        ...(eligibility ? { eligibility } : {}),
2097
        deltas,
2098
        conflicts,
2099
      };
2100
    })
2101
    .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
2102
}
2103
2104
function InteractionJourney({ journey }: { journey: InteractionJourneyModel }) {
2105
  const timeline = interactionTimeline(journey);
2106
  const conclusion = journey.conclusion;
2107
  const checkpoints = journey.events.filter(
2108
    (event) => (event.type ?? event.eventType) === 'progress_checkpoint' && event.checkpoint,
2109
  );
2110
  const latestCheckpoint = checkpoints.at(-1)?.checkpoint;
2111
  const progress = Number(latestCheckpoint?.progress ?? 0);
2112
  const submittedFeedback = new Set(
2113
    journey.feedback.map((item) => item.reviewerAgentId).filter(Boolean),
2114
  ).size;
2115
  return (
2116
    <article className="journeyDetail">
2117
      <header className="journeyHero">
2118
        <div>
2119
          <span className="eyebrow">{journey.taskCategory}</span>
2120
          <h2>{journey.purpose}</h2>
2121
          <p>{journey.participants.join(' ↔ ')}</p>
2122
        </div>
2123
        <StatusPill value={conclusion?.outcome ?? journey.status} />
2124
      </header>
2125
2126
      <div className="journeySummaryGrid">
2127
        <section>
2128
          <span>Progress</span>
2129
          <strong>
2130
            {latestCheckpoint
2131
              ? `${Math.round(progress * 100)}% · ${humanize(latestCheckpoint.state)}`
2132
              : journey.reports.length
2133
                ? 'One side ended'
2134
                : 'Awaiting checkpoint'}
2135
          </strong>
2136
          <small>
2137
            {latestCheckpoint
2138
              ? `${humanize(latestCheckpoint.topicStatus)} · ${Math.round(Number(latestCheckpoint.confidence ?? 0) * 100)}% confidence`
2139
              : 'Agents report compact progress every few exchanges'}
2140
          </small>
2141
        </section>
2142
        <section>
2143
          <span>Outcome</span>
2144
          <strong>
2145
            {conclusion
2146
              ? humanize(conclusion.outcome)
2147
              : `${journey.reports.length}/2 terminal reports`}
2148
          </strong>
2149
          <small>
2150
            {conclusion
2151
              ? conclusion.lifecycle === 'provisional'
2152
                ? `${Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer ${humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()}`
2153
                : humanize(conclusion.consensus)
2154
              : journey.reports.length === 1
2155
                ? 'One agent finished; confirming with the peer'
2156
                : 'Conversation remains active'}
2157
          </small>
2158
        </section>
2159
        <section>
2160
          <span>Sealed feedback</span>
2161
          <strong>
2162
            {journey.feedbackRequests.some((request) => request.status === 'pending')
2163
              ? `${submittedFeedback}/2 sealed`
2164
              : 'Window closed'}
2165
          </strong>
2166
          <small>{feedbackState(journey.feedbackRequests)}</small>
2167
        </section>
2168
        <section>
2169
          <span>Learning</span>
2170
          <strong>
2171
            {journey.eligibility
2172
              ? journey.eligibility.eligible
2173
                ? `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% weight`
2174
                : 'Not eligible'
2175
              : conclusion?.lifecycle === 'provisional'
2176
                ? 'Pending corroboration'
2177
                : 'Not assessed'}
2178
          </strong>
2179
          <small>{humanize(journey.eligibility?.contributionMode ?? 'Local only')}</small>
2180
        </section>
2181
      </div>
2182
2183
      {latestCheckpoint ? (
2184
        <section className="progressCard">
2185
          <div className="progressCardTop">
2186
            <div>
2187
              <span className="eyebrow">LATEST PRIVATE CHECKPOINT</span>
2188
              <h3>{humanize(latestCheckpoint.state)}</h3>
2189
            </div>
2190
            <strong>{Math.round(progress * 100)}%</strong>
2191
          </div>
2192
          <div
2193
            className="progressTrack"
2194
            role="progressbar"
2195
            aria-label="Interaction progress"
2196
            aria-valuemin={0}
2197
            aria-valuemax={100}
2198
            aria-valuenow={Math.round(progress * 100)}
2199
          >
2200
            <i style={{ width: `${Math.round(progress * 100)}%` }} />
2201
          </div>
2202
          <div className="progressFacts">
2203
            <span>{latestCheckpoint.criteriaMet?.length ?? 0} criteria met</span>
2204
            <span>{latestCheckpoint.criteriaRemaining?.length ?? 0} remaining</span>
2205
            <span>{latestCheckpoint.expectedRemainingTurns ?? '—'} expected turns</span>
2206
            <span>
2207
              {latestCheckpoint.needsHuman ? 'Human input needed' : 'No human input needed'}
2208
            </span>
2209
          </div>
2210
          {latestCheckpoint.topicStatus !== 'in_scope' ? (
2211
            <p className="progressWarning">
2212
              Topic {humanize(latestCheckpoint.topicStatus)}. Amend the contract or start a new
2213
              interaction before continuing.
2214
            </p>
2215
          ) : null}
2216
        </section>
2217
      ) : null}
2218
2219
      {conclusion ? (
2220
        <section className="outcomeCard">
2221
          <div>
2222
            <span className="eyebrow">
2223
              {conclusion.lifecycle === 'provisional' ? 'PROVISIONAL INSIGHT' : 'WHAT HAPPENED'}
2224
            </span>
2225
            <h3>{conclusion.summary}</h3>
2226
            {conclusion.lifecycle === 'provisional' ? (
2227
              <p className="progressWarning">
2228
                Based on {conclusion.reportIds?.length ?? 1} participant report. Missing:{' '}
2229
                {(conclusion.missingReportAgentIds ?? []).join(', ') || 'peer report'}. This is
2230
                usable now at reduced confidence and will be revised if the peer responds.
2231
              </p>
2232
            ) : null}
2233
          </div>
2234
          {!!conclusion.criteria?.length && (
2235
            <ul>
2236
              {conclusion.criteria.map((criterion: Record<string, any>) => (
2237
                <li key={criterion.criterion}>
2238
                  <StatusPill value={String(criterion.status)} />
2239
                  <span>{criterion.criterion}</span>
2240
                </li>
2241
              ))}
2242
            </ul>
2243
          )}
2244
          {!!Object.keys(conclusion.averageRatings ?? {}).length && (
2245
            <div className="ratingGrid">
2246
              {Object.entries(conclusion.averageRatings).map(([label, value]) => (
2247
                <Meter key={label} label={humanize(label)} value={Number(value)} />
2248
              ))}
2249
            </div>
2250
          )}
2251
        </section>
2252
      ) : (
2253
        <div className="journeyNotice">
2254
          {journey.reports.length === 1
2255
            ? 'Finalizing: one agent submitted its terminal report. OpenClasp has requested independent confirmation from the peer.'
2256
            : journey.reports.length >= 2
2257
              ? 'Both agents finished. Sealed feedback remains private until both respond or the feedback window expires.'
2258
              : 'Active interaction. Progress checkpoints appear after several meaningful exchanges; either agent can finalize immediately when the task is done.'}
2259
        </div>
2260
      )}
2261
2262
      <section className="journeyTimelineSection">
2263
        <div>
2264
          <h3>Interaction timeline</h3>
2265
          <small>Contract → session → outcome → feedback → learning</small>
2266
        </div>
2267
        <ol className="journeyTimeline">
2268
          {timeline.map((step, index) => (
2269
            <li key={`${step.at}-${step.title}-${index}`}>
2270
              <span className={`journeyNode ${step.tone}`} />
2271
              <div>
2272
                <span>{formatDate(step.at)}</span>
2273
                <strong>{step.title}</strong>
2274
                <p>{step.detail}</p>
2275
              </div>
2276
              <StatusPill value={step.status} />
2277
            </li>
2278
          ))}
2279
        </ol>
2280
      </section>
2281
    </article>
2282
  );
2283
}
2284
2285
function interactionTimeline(journey: InteractionJourneyModel) {
2286
  const revisions = journey.interaction.contractRevisions ?? [];
2287
  const contractSteps = revisions.length
2288
    ? revisions.map((revision: Record<string, any>) => ({
2289
        at: String(revision.updatedAt ?? revision.createdAt ?? journey.interaction.createdAt),
2290
        title: `Contract v${revision.revision} ${humanize(revision.status)}`,
2291
        detail: `${revision.proposedByAgentId} · ${Object.keys(revision.acceptances ?? {}).length}/${journey.participants.length || 2} accepted · ${String(revision.termsHash).slice(0, 12)}…`,
2292
        status: String(revision.status),
2293
        tone:
2294
          revision.status === 'accepted'
2295
            ? 'good'
2296
            : revision.status === 'rejected'
2297
              ? 'danger'
2298
              : revision.status === 'proposed'
2299
                ? 'warn'
2300
                : 'neutral',
2301
      }))
2302
    : [
2303
        {
2304
          at: String(journey.interaction.createdAt ?? journey.updatedAt),
2305
          title: 'Contract proposed',
2306
          detail: `${journey.participants.length || 2} participants · ${journey.taskCategory}`,
2307
          status: String(journey.interaction.status ?? 'recorded'),
2308
          tone: 'neutral',
2309
        },
2310
        ...Object.values(journey.interaction.acceptances ?? {}).map((acceptance: any) => ({
2311
          at: String(acceptance.acceptedAt ?? journey.interaction.updatedAt ?? journey.updatedAt),
2312
          title: `Contract accepted by ${acceptance.agentId}`,
2313
          detail: `Acceptance method: ${humanize(acceptance.method ?? 'recorded')}`,
2314
          status: 'accepted',
2315
          tone: 'good',
2316
        })),
2317
      ];
2318
  const steps: { at: string; title: string; detail: string; status: string; tone: string }[] = [
2319
    ...contractSteps,
2320
    ...journey.briefs.map((brief) => ({
2321
      at: String(brief.generatedAt ?? journey.updatedAt),
2322
      title: `Private counterparty brief for ${brief.recipientAgentId}`,
2323
      detail: `${brief.relevantSampleSize ?? 0} relevant samples · ${Math.round(Number(brief.historyConfidence ?? 0) * 100)}% history confidence`,
2324
      status: String(brief.decision ?? 'ready'),
2325
      tone: brief.decision === 'DENY' ? 'danger' : brief.decision === 'CHALLENGE' ? 'warn' : 'good',
2326
    })),
2327
    ...(journey.session?.activatedAt
2328
      ? [
2329
          {
2330
            at: String(journey.session.activatedAt),
2331
            title: 'A2A session activated',
2332
            detail:
2333
              'Scoped credentials were issued. Agent runtimes exchange conversation content directly.',
2334
            status: 'active',
2335
            tone: 'good',
2336
          },
2337
        ]
2338
      : []),
2339
    ...journey.events.map((event) => {
2340
      const eventType = String(event.type ?? event.eventType ?? 'structured_event');
2341
      const checkpoint = event.checkpoint;
2342
      return {
2343
        at: timestamp(event),
2344
        title:
2345
          eventType === 'progress_checkpoint'
2346
            ? `${Math.round(Number(checkpoint?.progress ?? 0) * 100)}% progress checkpoint`
2347
            : humanize(eventType),
2348
        detail:
2349
          eventType === 'progress_checkpoint'
2350
            ? `${event.agentId} · ${humanize(checkpoint?.state ?? 'active')} · ${humanize(checkpoint?.topicStatus ?? 'in_scope')}`
2351
            : String(event.agentId ?? 'OpenClasp') + ' submitted structured metadata.',
2352
        status: String(checkpoint?.state ?? event.visibility ?? 'recorded'),
2353
        tone:
2354
          checkpoint?.topicStatus === 'changed' || checkpoint?.state === 'blocked'
2355
            ? 'warn'
2356
            : eventType.includes('violation')
2357
              ? 'danger'
2358
              : eventType.includes('warning')
2359
                ? 'warn'
2360
                : checkpoint
2361
                  ? 'good'
2362
                  : 'neutral',
2363
      };
2364
    }),
2365
    ...journey.reports.map((report) => ({
2366
      at: timestamp(report),
2367
      title: `Completion reported by ${report.reportingAgentId}`,
2368
      detail: String(report.summary ?? 'Structured completion report submitted.'),
2369
      status: String(report.outcome ?? 'reported'),
2370
      tone:
2371
        report.outcome === 'failure' ? 'danger' : report.outcome === 'partial' ? 'warn' : 'good',
2372
    })),
2373
    ...journey.feedback.map((item) => ({
2374
      at: timestamp(item),
2375
      title: `Feedback submitted by ${item.reviewerAgentId}`,
2376
      detail: `Would work again: ${humanize(item.wouldWorkAgain ?? 'unsure')} · private comment concealed`,
2377
      status: 'sealed',
2378
      tone: 'neutral',
2379
    })),
2380
    ...journey.feedbackRequests
2381
      .filter((request) => request.status !== 'submitted')
2382
      .map((request) => ({
2383
        at: String(request.requestedAt ?? journey.updatedAt),
2384
        title: `Feedback request ${humanize(request.status)}`,
2385
        detail:
2386
          request.status === 'pending'
2387
            ? `Response due ${formatDate(String(request.dueAt))}`
2388
            : 'The bilateral conclusion continued without this response.',
2389
        status: String(request.status),
2390
        tone: request.status === 'pending' ? 'warn' : 'neutral',
2391
      })),
2392
    ...journey.conflicts.map((conflict) => ({
2393
      at: timestamp(conflict),
2394
      title: 'Dispute recorded',
2395
      detail: String(conflict.summary ?? 'The interaction has a structured dispute record.'),
2396
      status: String(conflict.status ?? 'open'),
2397
      tone: conflict.status === 'resolved' ? 'good' : 'danger',
2398
    })),
2399
    ...(journey.conclusion
2400
      ? [
2401
          {
2402
            at: String(journey.conclusion.generatedAt),
2403
            title:
2404
              journey.conclusion.lifecycle === 'provisional'
2405
                ? 'Provisional insight generated'
2406
                : 'Conclusion released',
2407
            detail: String(journey.conclusion.summary),
2408
            status: String(journey.conclusion.consensus),
2409
            tone:
2410
              journey.conclusion.lifecycle === 'provisional' ||
2411
              journey.conclusion.consensus === 'conflicting'
2412
                ? 'warn'
2413
                : 'good',
2414
          },
2415
        ]
2416
      : []),
2417
    ...(journey.receipt
2418
      ? [
2419
          {
2420
            at: timestamp(journey.receipt),
2421
            title: journey.receipt.provisional
2422
              ? 'Provisional outcome attested'
2423
              : 'Outcome receipt attested',
2424
            detail: journey.receipt.provisional
2425
              ? 'The available one-sided report was sealed without claiming peer agreement.'
2426
              : 'Contract result, commitment status, and evidence hashes were sealed.',
2427
            status: String(journey.receipt.outcome ?? 'signed'),
2428
            tone: journey.receipt.outcome === 'failure' ? 'danger' : 'good',
2429
          },
2430
        ]
2431
      : []),
2432
    ...(journey.eligibility
2433
      ? [
2434
          {
2435
            at: String(journey.eligibility.decidedAt),
2436
            title: journey.eligibility.eligible
2437
              ? 'Eligible history applied'
2438
              : 'History excluded from profile',
2439
            detail: `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% sample weight · ${humanize(journey.eligibility.contributionMode)}`,
2440
            status: journey.eligibility.eligible ? 'attested' : 'excluded',
2441
            tone: journey.eligibility.eligible ? 'good' : 'warn',
2442
          },
2443
        ]
2444
      : []),
2445
  ];
2446
  return steps.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
2447
}
2448
2449
function Agents({
2450
  data,
2451
  navigate,
2452
  refreshDashboard,
2453
  api,
2454
}: {
2455
  data: DashboardData;
2456
  navigate: (page: Page) => void;
2457
  refreshDashboard: () => Promise<void>;
2458
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2459
}) {
2460
  const [working, setWorking] = useState('');
2461
  const [error, setError] = useState('');
2462
  const saveAutomation = async (
2463
    agentId: string,
2464
    value: {
2465
      autoPublish: boolean;
2466
      autoAcceptPolicy: 'off' | 'safe_matching';
2467
      autoAcceptTaskCategories: string[];
2468
    },
2469
  ) => {
2470
    setWorking(agentId);
2471
    setError('');
2472
    try {
2473
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/automation`, {
2474
        method: 'PUT',
2475
        body: JSON.stringify(value),
2476
      });
2477
      await refreshDashboard();
2478
      return true;
2479
    } catch (reason) {
2480
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
2481
      return false;
2482
    } finally {
2483
      setWorking('');
2484
    }
2485
  };
2486
  const saveRuntime = async (agentId: string, endpoint: string) => {
2487
    setWorking(`runtime:${agentId}`);
2488
    setError('');
2489
    try {
2490
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, {
2491
        method: 'PUT',
2492
        body: JSON.stringify({ endpoint }),
2493
      });
2494
      await refreshDashboard();
2495
    } catch (reason) {
2496
      setError(reason instanceof Error ? reason.message : 'Runtime verification failed');
2497
    } finally {
2498
      setWorking('');
2499
    }
2500
  };
2501
  const disableRuntime = async (agentId: string) => {
2502
    setWorking(`runtime:${agentId}`);
2503
    setError('');
2504
    try {
2505
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, { method: 'DELETE' });
2506
      await refreshDashboard();
2507
    } catch (reason) {
2508
      setError(reason instanceof Error ? reason.message : 'Runtime update failed');
2509
    } finally {
2510
      setWorking('');
2511
    }
2512
  };
2513
  const revokeAccessToken = async (agentId: string, tokenId: string) => {
2514
    setWorking(`token:${agentId}`);
2515
    setError('');
2516
    try {
2517
      await api(
2518
        `/v0.1/agents/${encodeURIComponent(agentId)}/access-tokens/${encodeURIComponent(tokenId)}`,
2519
        { method: 'DELETE' },
2520
      );
2521
      await refreshDashboard();
2522
    } catch (reason) {
2523
      setError(reason instanceof Error ? reason.message : 'Could not revoke agent access token');
2524
    } finally {
2525
      setWorking('');
2526
    }
2527
  };
2528
  const deleteAgent = async (agent: Record<string, any>) => {
2529
    const agentId = String(agent.agentId);
2530
    if (
2531
      !window.confirm(
2532
        `Delete “${String(agent.name ?? agentId)}”?\n\nIts runtime, publication, presence and MCP binding will be removed. Signed interaction and receipt history will be retained.`,
2533
      )
2534
    )
2535
      return;
2536
    setWorking(`delete:${agentId}`);
2537
    setError('');
2538
    try {
2539
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
2540
      await refreshDashboard();
2541
    } catch (reason) {
2542
      setError(reason instanceof Error ? reason.message : 'Agent deletion failed');
2543
    } finally {
2544
      setWorking('');
2545
    }
2546
  };
2547
  return (
2548
    <>
2549
      <PageHead page="agents" action="Connect agent" onAction={() => navigate('connect')} />
2550
      <div className="notice">
2551
        <strong>Direct A2A only.</strong> Conversation bodies travel between agent-owned runtimes.
2552
        OpenClasp does not proxy or retain them.
2553
      </div>
2554
      {error && (
2555
        <div className="errorBar" role="alert">
2556
          {error}
2557
        </div>
2558
      )}
2559
      <section className="agentGrid">
2560
        {data.agents.length ? (
2561
          data.agents.map((agent) => (
2562
            <AgentCard
2563
              key={agent.agentId}
2564
              agent={agent}
2565
              projectName={
2566
                data.projects.find((project) => project.projectId === agent.projectId)?.name
2567
              }
2568
              published={data.publications.some(
2569
                (publication) => publication.agentId === agent.agentId && publication.published,
2570
              )}
2571
              working={working === agent.agentId}
2572
              onSave={(value) => saveAutomation(agent.agentId, value)}
2573
              runtime={data.runtimes.find((runtime) => runtime.agentId === agent.agentId)}
2574
              runtimeWorking={working === `runtime:${agent.agentId}`}
2575
              deleteWorking={working === `delete:${agent.agentId}`}
2576
              onRuntime={(endpoint) => saveRuntime(agent.agentId, endpoint)}
2577
              onDisableRuntime={() => disableRuntime(agent.agentId)}
2578
              accessTokens={data.accessTokens.filter(
2579
                (token) =>
2580
                  token.agentId === agent.agentId &&
2581
                  !token.revokedAt &&
2582
                  Date.parse(String(token.expiresAt)) > Date.now(),
2583
              )}
2584
              accessTokenWorking={working === `token:${agent.agentId}`}
2585
              onRevokeAccessToken={(tokenId) => revokeAccessToken(agent.agentId, tokenId)}
2586
              onDelete={() => deleteAgent(agent)}
2587
            />
2588
          ))
2589
        ) : (
2590
          <Empty
2591
            title="No agents connected"
2592
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
2593
            action="Open connection guide"
2594
            onAction={() => navigate('connect')}
2595
          />
2596
        )}
2597
      </section>
2598
    </>
2599
  );
2600
}
2601
2602
function Invitations({
2603
  data,
2604
  refreshDashboard,
2605
  api,
2606
}: {
2607
  data: DashboardData;
2608
  refreshDashboard: () => Promise<void>;
2609
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2610
}) {
2611
  const [working, setWorking] = useState('');
2612
  const [error, setError] = useState('');
2613
  const ownedAgentIds = useMemo(
2614
    () => new Set(data.agents.map((agent) => String(agent.agentId))),
2615
    [data.agents],
2616
  );
2617
  const currentProposal = (interaction: Record<string, any>) =>
2618
    [...(interaction.contractRevisions ?? [])]
2619
      .reverse()
2620
      .find((revision) => revision.status === 'proposed');
2621
  const respondingAgent = (interaction: Record<string, any>) => {
2622
    const proposal = currentProposal(interaction);
2623
    return (interaction.contract?.parties ?? []).find(
2624
      (agentId: string) => ownedAgentIds.has(String(agentId)) && !proposal?.acceptances?.[agentId],
2625
    );
2626
  };
2627
  const incoming = data.federatedInteractions.filter(
2628
    (interaction) => currentProposal(interaction) && respondingAgent(interaction),
2629
  );
2630
  const respond = async (
2631
    interactionId: string,
2632
    revisionId: string,
2633
    agentId: string,
2634
    decision: 'accept' | 'reject',
2635
  ) => {
2636
    setWorking(interactionId);
2637
    setError('');
2638
    try {
2639
      await api(
2640
        `/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals/${encodeURIComponent(revisionId)}/respond`,
2641
        {
2642
          method: 'POST',
2643
          body: JSON.stringify({ agentId, decision }),
2644
        },
2645
      );
2646
      await refreshDashboard();
2647
    } catch (reason) {
2648
      setError(reason instanceof Error ? reason.message : 'Could not respond to invitation');
2649
    } finally {
2650
      setWorking('');
2651
    }
2652
  };
2653
  if (!incoming.length && !data.federatedInteractions.length) return null;
2654
  return (
2655
    <Panel
2656
      title={incoming.length ? `Contract decisions (${incoming.length})` : 'Shared interactions'}
2657
      subtitle=""
2658
    >
2659
      {error ? (
2660
        <div className="errorBar" role="alert">
2661
          {error}
2662
        </div>
2663
      ) : null}
2664
      {(incoming.length ? incoming : data.federatedInteractions.slice(0, 5)).map((interaction) => (
2665
        <article className="setupRequest" key={interaction.interactionId}>
2666
          <div>
2667
            <strong>{interaction.contract?.purpose ?? interaction.interactionId}</strong>
2668
            <small>
2669
              {interaction.initiatorAgentId} → {interaction.responderAgentId} · {interaction.status}
2670
            </small>
2671
            <small>
2672
              Contract v{interaction.contractRevision ?? 1}:{' '}
2673
              {String(currentProposal(interaction)?.termsHash ?? interaction.termsHash).slice(
2674
                0,
2675
                16,
2676
              )}
2677
              …
2678
              {currentProposal(interaction)
2679
                ? ` · proposed by ${currentProposal(interaction).proposedByAgentId}`
2680
                : ' · agreed'}
2681
            </small>
2682
            {interaction.contractRevisions?.length > 1 ? (
2683
              <small>{interaction.contractRevisions.length} recorded contract revisions</small>
2684
            ) : null}
2685
            {data.liveSessions.find(
2686
              (session) => session.interactionId === interaction.interactionId,
2687
            ) ? (
2688
              <small className="autoAccepted">
2689
                Live session:{' '}
2690
                {
2691
                  data.liveSessions.find(
2692
                    (session) => session.interactionId === interaction.interactionId,
2693
                  )?.status
2694
                }
2695
              </small>
2696
            ) : null}
2697
            {interaction.status === 'active' &&
2698
            interaction.acceptances?.[interaction.responderAgentId]?.method ===
2699
              'policy_auto_accept' ? (
2700
              <small className="autoAccepted">✓ Auto-approved by the responder's safe policy</small>
2701
            ) : null}
2702
          </div>
2703
          {currentProposal(interaction) && respondingAgent(interaction) ? (
2704
            <div className="decisionButtons">
2705
              <button
2706
                className="secondary"
2707
                type="button"
2708
                disabled={working === interaction.interactionId}
2709
                onClick={() =>
2710
                  void respond(
2711
                    interaction.interactionId,
2712
                    currentProposal(interaction).revisionId,
2713
                    respondingAgent(interaction),
2714
                    'reject',
2715
                  )
2716
                }
2717
              >
2718
                Reject
2719
              </button>
2720
              <button
2721
                className="primary"
2722
                type="button"
2723
                disabled={working === interaction.interactionId}
2724
                onClick={() =>
2725
                  void respond(
2726
                    interaction.interactionId,
2727
                    currentProposal(interaction).revisionId,
2728
                    respondingAgent(interaction),
2729
                    'accept',
2730
                  )
2731
                }
2732
              >
2733
                {working === interaction.interactionId
2734
                  ? 'Working…'
2735
                  : interaction.status === 'active'
2736
                    ? 'Accept amendment'
2737
                    : 'Accept contract'}
2738
              </button>
2739
            </div>
2740
          ) : null}
2741
        </article>
2742
      ))}
2743
    </Panel>
2744
  );
2745
}
2746
2747
function Insights({ data }: { data: DashboardData }) {
2748
  const eligible = data.learningEligibility.filter((item) => item.eligible);
2749
  const pendingFeedback = data.feedbackRequests.filter((item) => item.status === 'pending');
2750
  const provisional = data.interactionConclusions.filter(
2751
    (item) => item.lifecycle === 'provisional',
2752
  );
2753
  return (
2754
    <>
2755
      <PageHead page="insights" />
2756
      <section className="insightPrinciple">
2757
        <div>
2758
          <h2>Reliability</h2>
2759
          <p>
2760
            Every profile is tied to an agent version and task category. Scores reflect eligible
2761
            structured outcomes and decay as history gets older.
2762
          </p>
2763
        </div>
2764
        <div className="insightStats">
2765
          <span>
2766
            <strong>{data.profiles.length}</strong> contextual profiles
2767
          </span>
2768
          <span>
2769
            <strong>{eligible.length}</strong> eligible interactions
2770
          </span>
2771
          <span>
2772
            <strong>{pendingFeedback.length}</strong> feedback pending
2773
          </span>
2774
          <span>
2775
            <strong>{provisional.length}</strong> provisional signals
2776
          </span>
2777
        </div>
2778
      </section>
2779
      {!!provisional.length && (
2780
        <Panel
2781
          title="Provisional interaction signals"
2782
          subtitle="Available now, kept separate from corroborated reliability history"
2783
        >
2784
          <div className="learningDecisionList">
2785
            {provisional
2786
              .slice()
2787
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
2788
              .map((conclusion) => (
2789
                <article key={conclusion.conclusionId}>
2790
                  <StatusPill value="provisional" />
2791
                  <div>
2792
                    <strong>{conclusion.summary}</strong>
2793
                    <small>
2794
                      {Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer{' '}
2795
                      {humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()} ·{' '}
2796
                      {relativeTime(timestamp(conclusion))}
2797
                    </small>
2798
                    <p>
2799
                      Missing report from{' '}
2800
                      {(conclusion.missingReportAgentIds ?? []).join(', ') || 'the counterparty'}.
2801
                      The signal will be revised if more structured evidence arrives.
2802
                    </p>
2803
                  </div>
2804
                </article>
2805
              ))}
2806
          </div>
2807
        </Panel>
2808
      )}
2809
      <section className="insightGrid">
2810
        {data.profiles.length ? (
2811
          data.profiles.map((profile) => {
2812
            const deltas = data.profileDeltas
2813
              .filter(
2814
                (delta) =>
2815
                  delta.agentId === profile.agentId &&
2816
                  delta.agentVersion === profile.agentVersion &&
2817
                  delta.taskCategory === profile.taskCategory,
2818
              )
2819
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)));
2820
            return (
2821
              <Panel
2822
                key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
2823
                title={profile.agentId}
2824
                subtitle={`${humanize(profile.taskCategory)} · version ${profile.agentVersion}`}
2825
              >
2826
                <Profile profile={profile} expanded deltas={deltas} showHeader={false} />
2827
              </Panel>
2828
            );
2829
          })
2830
        ) : (
2831
          <Empty
2832
            title="Not enough verified evidence"
2833
            text="Insights grow only from authenticated outcomes, eligible feedback, corrections, disputes, and version-aware history."
2834
          />
2835
        )}
2836
      </section>
2837
      {!!data.learningEligibility.length && (
2838
        <Panel
2839
          title="Recent learning decisions"
2840
          subtitle="Why an interaction did or did not affect contextual history"
2841
        >
2842
          <div className="learningDecisionList">
2843
            {data.learningEligibility
2844
              .slice()
2845
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
2846
              .slice(0, 8)
2847
              .map((decision) => (
2848
                <article key={decision.decisionId}>
2849
                  <StatusPill value={decision.eligible ? 'eligible' : 'excluded'} />
2850
                  <div>
2851
                    <strong>{decision.interactionId}</strong>
2852
                    <small>
2853
                      {Math.round(Number(decision.sampleWeight ?? 0) * 100)}% weight ·{' '}
2854
                      {humanize(decision.contributionMode)} · {relativeTime(timestamp(decision))}
2855
                    </small>
2856
                    <p>{decision.reasons?.[0] ?? 'No reason supplied.'}</p>
2857
                  </div>
2858
                </article>
2859
              ))}
2860
          </div>
2861
        </Panel>
2862
      )}
2863
    </>
2864
  );
2865
}
2866
2867
function Connect({
2868
  data,
2869
  navigate,
2870
  refreshDashboard,
2871
  api,
2872
}: {
2873
  data: DashboardData;
2874
  navigate: (page: Page) => void;
2875
  refreshDashboard: () => Promise<void>;
2876
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2877
}) {
2878
  const endpoint = `${new URL(__OPENCLASP_PUBLIC_URL__).origin}/mcp`;
2879
  const [connectionType, setConnectionType] = useState<'interactive' | 'hosted'>('hosted');
2880
  const [copied, setCopied] = useState(false);
2881
  const [tokenCopied, setTokenCopied] = useState(false);
2882
  const [working, setWorking] = useState('');
2883
  const [decisionError, setDecisionError] = useState('');
2884
  const [providerError, setProviderError] = useState('');
2885
  const [providerForm, setProviderForm] = useState({
2886
    provider: 'botpress' as 'botpress' | 'custom',
2887
    agentName: '',
2888
    projectName: '',
2889
    description: '',
2890
    capabilities: '',
2891
    limitations: '',
2892
    expiresInDays: 365,
2893
  });
2894
  const [providerResult, setProviderResult] = useState<{
2895
    provider: 'botpress' | 'custom';
2896
    agent: { agentId: string; name: string; framework: string };
2897
    accessToken: { token: string; expiresAt: string };
2898
  }>();
2899
  const pending = data.setupRequests.filter((request) => request.status === 'pending');
2900
  useEffect(() => {
2901
    const poll = () => void refreshDashboard().catch(() => undefined);
2902
    poll();
2903
    const timer = window.setInterval(poll, 5000);
2904
    return () => window.clearInterval(timer);
2905
  }, [refreshDashboard]);
2906
  const decide = async (requestId: string, decision: 'approve' | 'reject') => {
2907
    setWorking(requestId);
2908
    setDecisionError('');
2909
    try {
2910
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
2911
        method: 'POST',
2912
      });
2913
      await refreshDashboard();
2914
    } catch (reason) {
2915
      setDecisionError(reason instanceof Error ? reason.message : 'Could not update setup request');
2916
    } finally {
2917
      setWorking('');
2918
    }
2919
  };
2920
  const connectHostedProvider = async (event: React.FormEvent) => {
2921
    event.preventDefault();
2922
    setWorking('hosted-provider');
2923
    setProviderError('');
2924
    try {
2925
      const list = (value: string) =>
2926
        value
2927
          .split(/[,\n]/)
2928
          .map((item) => item.trim())
2929
          .filter(Boolean);
2930
      const created = (await api('/v0.1/provider-connections', {
2931
        method: 'POST',
2932
        body: JSON.stringify({
2933
          provider: providerForm.provider,
2934
          agentName: providerForm.agentName,
2935
          projectName: providerForm.projectName,
2936
          description: providerForm.description,
2937
          capabilities: list(providerForm.capabilities),
2938
          limitations: list(providerForm.limitations),
2939
          expiresInDays: providerForm.expiresInDays,
2940
        }),
2941
      })) as {
2942
        provider: 'botpress' | 'custom';
2943
        agent: { agentId: string; name: string; framework: string };
2944
        accessToken: { token: string; expiresAt: string };
2945
      };
2946
      setProviderResult(created);
2947
      await refreshDashboard().catch(() => undefined);
2948
    } catch (reason) {
2949
      setProviderError(reason instanceof Error ? reason.message : 'Could not create connection');
2950
    } finally {
2951
      setWorking('');
2952
    }
2953
  };
2954
  return (
2955
    <>
2956
      <PageHead page="connect" />
2957
      {pending.length > 0 && (
2958
        <section className="setupRequests">
2959
          <div>
2960
            <h2>Approve agent setup</h2>
2961
            <p>
2962
              An agent proposed this identity. Confirm it before OpenClasp binds the installation.
2963
            </p>
2964
          </div>
2965
          {pending.map((request) => (
2966
            <article className="setupRequest" key={request.requestId}>
2967
              <div>
2968
                <strong>{request.agentName ?? 'Switch installation'}</strong>
2969
                <small>
2970
                  {request.projectName ?? request.existingAgentId} · {request.framework}
2971
                </small>
2972
                {!!request.capabilities?.length && (
2973
                  <div className="tags">
2974
                    {request.capabilities.slice(0, 5).map((capability: string) => (
2975
                      <span key={capability}>{capability}</span>
2976
                    ))}
2977
                  </div>
2978
                )}
2979
                {!!request.limitations?.length && (
2980
                  <small>Limitations: {request.limitations.join(' · ')}</small>
2981
                )}
2982
                <div className="automationPreview">
2983
                  <span>
2984
                    {request.autoPublish ? 'Public after runtime verification' : 'Private'}
2985
                  </span>
2986
                  <span>Agent-owned runtime · direct A2A</span>
2987
                  <span>
2988
                    {request.autoAcceptPolicy === 'safe_matching'
2989
                      ? 'Auto-accept safe matches'
2990
                      : 'Review every request'}
2991
                  </span>
2992
                </div>
2993
              </div>
2994
              <div className="decisionButtons">
2995
                <button
2996
                  className="secondary"
2997
                  type="button"
2998
                  disabled={working === request.requestId}
2999
                  onClick={() => void decide(request.requestId, 'reject')}
3000
                >
3001
                  Reject
3002
                </button>
3003
                <button
3004
                  className="primary"
3005
                  type="button"
3006
                  disabled={working === request.requestId}
3007
                  onClick={() => void decide(request.requestId, 'approve')}
3008
                >
3009
                  {working === request.requestId ? 'Working…' : 'Approve & automate'}
3010
                </button>
3011
              </div>
3012
            </article>
3013
          ))}
3014
          {decisionError && (
3015
            <div className="loginError" role="alert">
3016
              {decisionError}
3017
            </div>
3018
          )}
3019
        </section>
3020
      )}
3021
      <div className="connectionTypePicker" role="tablist" aria-label="Agent connection type">
3022
        <button
3023
          type="button"
3024
          role="tab"
3025
          aria-selected={connectionType === 'hosted'}
3026
          className={connectionType === 'hosted' ? 'active' : ''}
3027
          onClick={() => setConnectionType('hosted')}
3028
        >
3029
          Hosted provider
3030
          <small>Botpress and similar platforms</small>
3031
        </button>
3032
        <button
3033
          type="button"
3034
          role="tab"
3035
          aria-selected={connectionType === 'interactive'}
3036
          className={connectionType === 'interactive' ? 'active' : ''}
3037
          onClick={() => setConnectionType('interactive')}
3038
        >
3039
          Interactive agent
3040
          <small>Codex, Cursor, and OAuth clients</small>
3041
        </button>
3042
      </div>
3043
      {connectionType === 'hosted' ? (
3044
        <section className="connectLayout providerConnectLayout">
3045
          <Panel title="Add a hosted agent" subtitle="Creates an isolated identity and credential">
3046
            {providerResult ? (
3047
              <div className="providerResult">
3048
                <div className="successBanner">
3049
                  <strong>{providerResult.agent.name} created</strong>
3050
                  <span>{providerResult.agent.agentId}</span>
3051
                </div>
3052
                <label>
3053
                  <span>MCP URL</span>
3054
                  <code>{endpoint}</code>
3055
                </label>
3056
                <label>
3057
                  <span>Agent token — copy it now; it will not be shown again</span>
3058
                  <code>{providerResult.accessToken.token}</code>
3059
                </label>
3060
                <small>
3061
                  This agent-bound token authorizes MCP and automatic runtime registration. It
3062
                  cannot connect or manage another agent.
3063
                </small>
3064
                <div className="agentActions">
3065
                  <button
3066
                    className="secondary"
3067
                    type="button"
3068
                    onClick={() => {
3069
                      void navigator.clipboard.writeText(providerResult.accessToken.token);
3070
                      setTokenCopied(true);
3071
                      window.setTimeout(() => setTokenCopied(false), 2000);
3072
                    }}
3073
                  >
3074
                    {tokenCopied ? 'Copied' : 'Copy token'}
3075
                  </button>
3076
                  <button className="primary" type="button" onClick={() => navigate('agents')}>
3077
                    View agent
3078
                  </button>
3079
                </div>
3080
              </div>
3081
            ) : (
3082
              <form
3083
                className="providerForm"
3084
                onSubmit={(event) => void connectHostedProvider(event)}
3085
              >
3086
                <label>
3087
                  <span>Provider</span>
3088
                  <select
3089
                    value={providerForm.provider}
3090
                    onChange={(event) =>
3091
                      setProviderForm((value) => ({
3092
                        ...value,
3093
                        provider: event.target.value as 'botpress' | 'custom',
3094
                      }))
3095
                    }
3096
                  >
3097
                    <option value="botpress">Botpress</option>
3098
                    <option value="custom">Custom / self-hosted</option>
3099
                  </select>
3100
                </label>
3101
                <label>
3102
                  <span>Agent name</span>
3103
                  <input
3104
                    required
3105
                    maxLength={100}
3106
                    value={providerForm.agentName}
3107
                    onChange={(event) =>
3108
                      setProviderForm((value) => ({ ...value, agentName: event.target.value }))
3109
                    }
3110
                    placeholder="Recruiting agent"
3111
                  />
3112
                </label>
3113
                <label>
3114
                  <span>Project</span>
3115
                  <input
3116
                    required
3117
                    maxLength={100}
3118
                    value={providerForm.projectName}
3119
                    onChange={(event) =>
3120
                      setProviderForm((value) => ({ ...value, projectName: event.target.value }))
3121
                    }
3122
                    placeholder="Recruiting"
3123
                  />
3124
                </label>
3125
                <label className="fullWidth">
3126
                  <span>Purpose</span>
3127
                  <textarea
3128
                    required
3129
                    maxLength={500}
3130
                    value={providerForm.description}
3131
                    onChange={(event) =>
3132
                      setProviderForm((value) => ({ ...value, description: event.target.value }))
3133
                    }
3134
                    placeholder="Matches suitable candidates with open roles"
3135
                  />
3136
                </label>
3137
                <label>
3138
                  <span>Capabilities, comma-separated</span>
3139
                  <input
3140
                    required
3141
                    value={providerForm.capabilities}
3142
                    onChange={(event) =>
3143
                      setProviderForm((value) => ({ ...value, capabilities: event.target.value }))
3144
                    }
3145
                    placeholder="candidate matching, interview coordination"
3146
                  />
3147
                </label>
3148
                <label>
3149
                  <span>Limitations, comma-separated</span>
3150
                  <input
3151
                    value={providerForm.limitations}
3152
                    onChange={(event) =>
3153
                      setProviderForm((value) => ({ ...value, limitations: event.target.value }))
3154
                    }
3155
                    placeholder="no final hiring decisions"
3156
                  />
3157
                </label>
3158
                <label>
3159
                  <span>Credential expiry</span>
3160
                  <select
3161
                    value={providerForm.expiresInDays}
3162
                    onChange={(event) =>
3163
                      setProviderForm((value) => ({
3164
                        ...value,
3165
                        expiresInDays: Number(event.target.value),
3166
                      }))
3167
                    }
3168
                  >
3169
                    <option value={30}>30 days</option>
3170
                    <option value={90}>90 days</option>
3171
                    <option value={365}>1 year</option>
3172
                  </select>
3173
                </label>
3174
                <div className="providerSubmit fullWidth">
3175
                  <p>This does not reuse or modify your Codex agent.</p>
3176
                  <button
3177
                    className="primary"
3178
                    type="submit"
3179
                    disabled={working === 'hosted-provider'}
3180
                  >
3181
                    {working === 'hosted-provider' ? 'Creating…' : 'Create agent credentials'}
3182
                  </button>
3183
                </div>
3184
                {providerError ? (
3185
                  <div className="loginError fullWidth" role="alert">
3186
                    {providerError}
3187
                  </div>
3188
                ) : null}
3189
              </form>
3190
            )}
3191
          </Panel>
3192
          <Panel
3193
            title={
3194
              providerForm.provider === 'botpress' ? 'Configure Botpress' : 'Deploy the sidecar'
3195
            }
3196
            subtitle={
3197
              providerForm.provider === 'botpress'
3198
                ? 'MCP and inbound runtime are separate connections'
3199
                : 'Runs beside an agent on any cloud'
3200
            }
3201
          >
3202
            {providerForm.provider === 'botpress' ? (
3203
              <>
3204
                <div className="connectSteps">
3205
                  <div className="connectStep">
3206
                    <b>1</b>
3207
                    <span>Add the displayed MCP URL with Bearer authentication.</span>
3208
                  </div>
3209
                  <div className="connectStep">
3210
                    <b>2</b>
3211
                    <span>
3212
                      Paste the generated <code>oc_at_…</code> agent token.
3213
                    </span>
3214
                  </div>
3215
                  <div className="connectStep">
3216
                    <b>3</b>
3217
                    <span>Install the OpenClasp Botpress runtime integration for inbound A2A.</span>
3218
                  </div>
3219
                </div>
3220
                <div className="notice providerNotice">
3221
                  MCP proves outbound tool access only. Autonomous runtime becomes connected only
3222
                  after the Botpress integration registers its webhook successfully.
3223
                </div>
3224
              </>
3225
            ) : (
3226
              <>
3227
                <div className="connectSteps">
3228
                  <div className="connectStep">
3229
                    <b>1</b>
3230
                    <span>
3231
                      Deploy <code>Dockerfile.sidecar</code> beside the agent.
3232
                    </span>
3233
                  </div>
3234
                  <div className="connectStep">
3235
                    <b>2</b>
3236
                    <span>
3237
                      Set <code>OPENCLASP_AGENT_TOKEN</code>, <code>OPENCLASP_RUNTIME_URL</code>,
3238
                      and <code>AGENT_ADAPTER_URL</code>.
3239
                    </span>
3240
                  </div>
3241
                  <div className="connectStep">
3242
                    <b>3</b>
3243
                    <span>
3244
                      The sidecar verifies and registers itself; no endpoint paste is needed.
3245
                    </span>
3246
                  </div>
3247
                </div>
3248
                <div className="notice providerNotice">
3249
                  The application implements three internal POST hooks: session-offer,
3250
                  session-activated, and message. Agent-to-agent content remains off OpenClasp.
3251
                </div>
3252
              </>
3253
            )}
3254
          </Panel>
3255
        </section>
3256
      ) : (
3257
        <section className="connectLayout">
3258
          <Panel title="Connect with OAuth" subtitle="For interactive MCP clients">
3259
            <div className="endpoint">
3260
              <code>{endpoint}</code>
3261
              <button
3262
                type="button"
3263
                onClick={() => {
3264
                  void navigator.clipboard.writeText(endpoint);
3265
                  setCopied(true);
3266
                  window.setTimeout(() => setCopied(false), 2000);
3267
                }}
3268
              >
3269
                {copied ? 'Copied' : 'Copy'}
3270
              </button>
3271
            </div>
3272
            <div className="connectSteps">
3273
              <div className="connectStep">
3274
                <b>1</b>
3275
                <span>Add this MCP URL and complete OAuth.</span>
3276
              </div>
3277
              <div className="connectStep">
3278
                <b>2</b>
3279
                <span>
3280
                  Tell the agent: <code>Set yourself up on OpenClasp</code>.
3281
                </span>
3282
              </div>
3283
              <div className="connectStep">
3284
                <b>3</b>
3285
                <span>Approve the proposed identity here.</span>
3286
              </div>
3287
            </div>
3288
          </Panel>
3289
          <Panel title="MCP clients" subtitle="Identity and control plane only">
3290
            <ul className="checkList">
3291
              <li>OAuth opens in the browser</li>
3292
              <li>Each installation is explicitly approved</li>
3293
              <li>OpenClasp does not proxy conversation messages</li>
3294
              <li>Connect your cloud runtime before publishing</li>
3295
            </ul>
3296
          </Panel>
3297
        </section>
3298
      )}
3299
    </>
3300
  );
3301
}
3302
3303
function SettingsPage({
3304
  settings,
3305
  setSettings,
3306
  api,
3307
}: {
3308
  settings: Settings;
3309
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
3310
  api: (path: string, init?: RequestInit) => Promise<unknown>;
3311
}) {
3312
  const [saving, setSaving] = useState(false);
3313
  const [saved, setSaved] = useState(false);
3314
  const [error, setError] = useState('');
3315
  const save = useCallback(async () => {
3316
    setSaving(true);
3317
    setSaved(false);
3318
    setError('');
3319
    try {
3320
      const updated = (await api('/v0.1/settings', {
3321
        method: 'PUT',
3322
        body: JSON.stringify(settings),
3323
      })) as Settings;
3324
      setSettings(updated);
3325
      setSaved(true);
3326
    } catch (reason) {
3327
      setError(reason instanceof Error ? reason.message : 'Could not save settings');
3328
    } finally {
3329
      setSaving(false);
3330
    }
3331
  }, [api, settings, setSettings]);
3332
  return (
3333
    <>
3334
      <PageHead page="settings" />
3335
      <section className="settingsCard">
3336
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
3337
          <input
3338
            value={settings.displayName}
3339
            onChange={(event) =>
3340
              setSettings((value) => ({ ...value, displayName: event.target.value }))
3341
            }
3342
            placeholder="Team or operator name"
3343
          />
3344
        </Setting>
3345
        <Setting
3346
          label="Network contribution"
3347
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
3348
        >
3349
          <Toggle
3350
            checked={settings.contributionEnabled}
3351
            label="Network contribution"
3352
            onChange={(checked) =>
3353
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
3354
            }
3355
          />
3356
        </Setting>
3357
        <Setting
3358
          label="Conversation data"
3359
          description="Agent runtimes exchange messages directly. OpenClasp does not receive or store conversation bodies."
3360
        >
3361
          <span className="locked">NOT COLLECTED</span>
3362
        </Setting>
3363
        <div className="saveRow">
3364
          <button className="primary" type="button" onClick={() => void save()} disabled={saving}>
3365
            {saving ? 'Saving…' : 'Save settings'}
3366
          </button>
3367
          {saved && <span role="status">Saved</span>}
3368
          {error && (
3369
            <span className="loginError" role="alert">
3370
              {error}
3371
            </span>
3372
          )}
3373
        </div>
3374
      </section>
3375
    </>
3376
  );
3377
}
3378
3379
function PageHead({
3380
  page,
3381
  action,
3382
  onAction,
3383
}: {
3384
  page: Page;
3385
  action?: string;
3386
  onAction?: () => void;
3387
}) {
3388
  const meta = pageMeta[page];
3389
  return (
3390
    <header className="pageHead">
3391
      <h1>{meta.title}</h1>
3392
      {action && (
3393
        <button className="primary" type="button" onClick={onAction}>
3394
          {action.toLowerCase()} <span aria-hidden="true">→</span>
3395
        </button>
3396
      )}
3397
    </header>
3398
  );
3399
}
3400
function Metric({
3401
  label,
3402
  value,
3403
  note,
3404
  warn,
3405
}: {
3406
  label: string;
3407
  value: number;
3408
  note: string;
3409
  warn?: boolean;
3410
}) {
3411
  return (
3412
    <article className={warn ? 'metric warn' : 'metric'} title={note}>
3413
      <span>{label}</span>
3414
      <strong>{value}</strong>
3415
      <small>{note}</small>
3416
    </article>
3417
  );
3418
}
3419
function Panel({
3420
  title,
3421
  subtitle,
3422
  children,
3423
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
3424
  return (
3425
    <section className="panel">
3426
      <div className="panelHead">
3427
        <h2 className="panelKicker">
3428
          <span>//</span> {title.toLowerCase()}
3429
        </h2>
3430
        {subtitle ? <p>{subtitle}</p> : null}
3431
      </div>
3432
      {children}
3433
    </section>
3434
  );
3435
}
3436
function Timeline({ events }: { events: Record<string, any>[] }) {
3437
  return events.length ? (
3438
    <div className="timeline">
3439
      {events.map((event) => (
3440
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
3441
      ))}
3442
    </div>
3443
  ) : (
3444
    <Empty
3445
      title="Nothing recorded yet"
3446
      text="Activity appears after your connected agents submit authenticated structured events."
3447
    />
3448
  );
3449
}
3450
function HistoryRow({ item }: { item: Record<string, any> }) {
3451
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
3452
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
3453
  return (
3454
    <article className="historyRow">
3455
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
3456
      <div>
3457
        <strong>{kind}</strong>
3458
        <small>
3459
          {id} · {new Date(timestamp(item)).toLocaleString()}
3460
        </small>
3461
      </div>
3462
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
3463
    </article>
3464
  );
3465
}
3466
function AgentCard({
3467
  agent,
3468
  projectName,
3469
  published,
3470
  working,
3471
  onSave,
3472
  runtime,
3473
  runtimeWorking,
3474
  deleteWorking,
3475
  onRuntime,
3476
  onDisableRuntime,
3477
  accessTokens,
3478
  accessTokenWorking,
3479
  onRevokeAccessToken,
3480
  onDelete,
3481
}: {
3482
  agent: Record<string, any>;
3483
  projectName?: string;
3484
  published: boolean;
3485
  working: boolean;
3486
  onSave: (value: {
3487
    autoPublish: boolean;
3488
    autoAcceptPolicy: 'off' | 'safe_matching';
3489
    autoAcceptTaskCategories: string[];
3490
  }) => Promise<boolean>;
3491
  runtime: Record<string, any> | undefined;
3492
  runtimeWorking: boolean;
3493
  deleteWorking: boolean;
3494
  onRuntime: (endpoint: string) => void;
3495
  onDisableRuntime: () => void;
3496
  accessTokens: Record<string, any>[];
3497
  accessTokenWorking: boolean;
3498
  onRevokeAccessToken: (tokenId: string) => Promise<void>;
3499
  onDelete: () => void;
3500
}) {
3501
  const [editing, setEditing] = useState(false);
3502
  const [autoPublish, setAutoPublish] = useState(Boolean(agent.autoPublish ?? published));
3503
  const [autoAcceptPolicy, setAutoAcceptPolicy] = useState<'off' | 'safe_matching'>(
3504
    agent.autoAcceptPolicy === 'safe_matching' ? 'safe_matching' : 'off',
3505
  );
3506
  const [categories, setCategories] = useState<string>(
3507
    (agent.autoAcceptTaskCategories?.length
3508
      ? agent.autoAcceptTaskCategories
3509
      : (agent.capabilities ?? [])
3510
    ).join(', '),
3511
  );
3512
  const [runtimeEndpoint, setRuntimeEndpoint] = useState(String(runtime?.endpoint ?? ''));
3513
  const ready = published && runtime?.status === 'verified';
3514
  const online = agent.presence?.status === 'online';
3515
  const providerConnected = accessTokens.length > 0;
3516
  const endpoint = String(runtime?.a2aEndpoint ?? agent.a2aEndpoint ?? 'Connect a runtime first');
3517
  const identityLabel = agent.revoked
3518
    ? 'REVOKED'
3519
    : agent.identityMode === 'owner_managed'
3520
      ? 'OWNER CREATED'
3521
      : agent.identityMode === 'oauth_installation'
3522
        ? 'AUTHENTICATED'
3523
        : 'VERIFIED';
3524
  return (
3525
    <article className="agentCard">
3526
      <div className="agentTop">
3527
        <AgentMark name={String(agent.name ?? agent.agentId)} online={online} size="sm" />
3528
        <div className="agentBadges">
3529
          <b className={runtime?.status === 'verified' ? 'onlineBadge' : 'offlineBadge'}>
3530
            {runtime?.status === 'verified'
3531
              ? 'ENDPOINT VERIFIED'
3532
              : providerConnected
3533
                ? 'TOKEN ONLY'
3534
                : 'ENDPOINT MISSING'}
3535
          </b>
3536
          <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
3537
          <b className={ready ? 'readyBadge' : 'needsBadge'}>{ready ? 'READY' : 'SETUP NEEDED'}</b>
3538
        </div>
3539
      </div>
3540
      <h2>{agent.name ?? agent.agentId}</h2>
3541
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
3542
      <div className="tags">
3543
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
3544
          <span key={capability}>{capability}</span>
3545
        ))}
3546
      </div>
3547
      <small>
3548
        {agent.framework ? `${agent.framework} · ` : ''}
3549
        {agent.identityMode === 'owner_managed'
3550
          ? 'Owner-created · '
3551
          : agent.identityMode === 'oauth_installation'
3552
            ? 'OAuth-bound · '
3553
            : 'Ed25519 · '}
3554
        Created {new Date(agent.createdAt).toLocaleDateString()}
3555
      </small>
3556
      <small>
3557
        {agent.presence?.lastSeenAt
3558
          ? `Last agent activity ${new Date(agent.presence.lastSeenAt).toLocaleString()}`
3559
          : 'No agent activity recorded yet'}
3560
      </small>
3561
      {published ? (
3562
        <div className="agentPublicLinks">
3563
          <a href={`/a/${encodeURIComponent(agent.agentId)}`} target="_blank" rel="noreferrer">
3564
            Verified public profile
3565
          </a>
3566
          <a
3567
            href={`/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`}
3568
            target="_blank"
3569
            rel="noreferrer"
3570
          >
3571
            A2A Agent Card
3572
          </a>
3573
        </div>
3574
      ) : null}
3575
      <div className="automationSummary">
3576
        <span>{published ? 'Public discovery' : 'Private'}</span>
3577
        <span>Direct agent-owned A2A</span>
3578
        <span>
3579
          {agent.autoAcceptPolicy === 'safe_matching' ? 'Safe tasks automatic' : 'Manual approval'}
3580
        </span>
3581
      </div>
3582
      <div className="runtimeBox">
3583
        <div>
3584
          <strong>Autonomous runtime</strong>
3585
          <span className={runtime?.status === 'verified' ? 'runtimeLive' : 'runtimeMissing'}>
3586
            {runtime?.status === 'verified' ? 'VERIFIED' : 'NOT CONNECTED'}
3587
          </span>
3588
        </div>
3589
        <p>
3590
          {runtime?.status === 'verified'
3591
            ? 'The agent runtime registered itself successfully. Conversation bodies travel directly between agents.'
3592
            : providerConnected
3593
              ? 'MCP is connected, but inbound A2A is not. Install the provider connector or deploy the sidecar; it registers this endpoint automatically.'
3594
              : 'Install a provider connector, deploy the sidecar, or register a native A2A endpoint.'}
3595
        </p>
3596
        <input
3597
          type="url"
3598
          value={runtimeEndpoint}
3599
          onChange={(event) => setRuntimeEndpoint(event.target.value)}
3600
          placeholder="https://agent.example.com/openclasp"
3601
        />
3602
        <small>
3603
          Manual registration is only for a runtime that already implements OpenClasp A2A.
3604
        </small>
3605
        <div className="agentActions">
3606
          {runtime?.status === 'verified' ? (
3607
            <button
3608
              className="secondary"
3609
              type="button"
3610
              disabled={runtimeWorking}
3611
              onClick={onDisableRuntime}
3612
            >
3613
              Disable runtime
3614
            </button>
3615
          ) : null}
3616
          <button
3617
            className="primary"
3618
            type="button"
3619
            disabled={runtimeWorking || !runtimeEndpoint.trim()}
3620
            onClick={() => onRuntime(runtimeEndpoint.trim())}
3621
          >
3622
            {runtimeWorking ? 'Verifying…' : runtime ? 'Rotate connection' : 'Verify & connect'}
3623
          </button>
3624
        </div>
3625
        {runtime?.lastError ? <small>Last session error: {runtime.lastError}</small> : null}
3626
      </div>
3627
      {accessTokens.length ? (
3628
        <div className="accessTokenBox">
3629
          <div className="accessTokenHead">
3630
            <div>
3631
              <strong>Provider connections</strong>
3632
              <span>{accessTokens.length} ACTIVE</span>
3633
            </div>
3634
            <p>Created through Connect. Revoke a provider here if its credential is exposed.</p>
3635
          </div>
3636
          <div className="tokenList">
3637
            {accessTokens.map((token) => (
3638
              <div key={token.tokenId}>
3639
                <span>
3640
                  <strong>{token.name}</strong>
3641
                  <small>
3642
                    Created {new Date(token.createdAt).toLocaleDateString()} · expires{' '}
3643
                    {new Date(token.expiresAt).toLocaleDateString()}
3644
                    {token.lastUsedAt
3645
                      ? ` · used ${new Date(token.lastUsedAt).toLocaleString()}`
3646
                      : ' · never used'}
3647
                  </small>
3648
                </span>
3649
                <button
3650
                  className="secondary dangerButton"
3651
                  type="button"
3652
                  disabled={accessTokenWorking}
3653
                  onClick={() => {
3654
                    if (window.confirm(`Revoke “${String(token.name)}”? Botpress will disconnect.`))
3655
                      void onRevokeAccessToken(String(token.tokenId));
3656
                  }}
3657
                >
3658
                  Revoke
3659
                </button>
3660
              </div>
3661
            ))}
3662
          </div>
3663
        </div>
3664
      ) : null}
3665
      {editing ? (
3666
        <div className="automationForm">
3667
          <label>
3668
            <span>Agent-owned A2A endpoint</span>
3669
            <input type="url" value={endpoint} readOnly />
3670
          </label>
3671
          <label>
3672
            <span>Invitation policy</span>
3673
            <select
3674
              value={autoAcceptPolicy}
3675
              onChange={(event) =>
3676
                setAutoAcceptPolicy(event.target.value as 'off' | 'safe_matching')
3677
              }
3678
            >
3679
              <option value="safe_matching">Auto-accept safe matches</option>
3680
              <option value="off">Review every request</option>
3681
            </select>
3682
          </label>
3683
          <label>
3684
            <span>Safe task categories</span>
3685
            <input
3686
              value={categories}
3687
              onChange={(event) => setCategories(event.target.value)}
3688
              placeholder="research, planning, coding"
3689
            />
3690
          </label>
3691
          <label className="checkControl">
3692
            <input
3693
              type="checkbox"
3694
              checked={autoPublish}
3695
              onChange={(event) => setAutoPublish(event.target.checked)}
3696
            />
3697
            <span>Publish and keep Agent Card updated</span>
3698
          </label>
3699
          <div className="agentActions">
3700
            <button className="secondary" type="button" onClick={() => setEditing(false)}>
3701
              Cancel
3702
            </button>
3703
            <button
3704
              className="primary"
3705
              type="button"
3706
              disabled={working || agent.status === 'revoked'}
3707
              onClick={() =>
3708
                void onSave({
3709
                  autoPublish,
3710
                  autoAcceptPolicy,
3711
                  autoAcceptTaskCategories: categories
3712
                    .split(',')
3713
                    .map((value) => value.trim())
3714
                    .filter(Boolean),
3715
                }).then((ok) => {
3716
                  if (ok) setEditing(false);
3717
                })
3718
              }
3719
            >
3720
              {working ? 'Saving…' : 'Save automation'}
3721
            </button>
3722
          </div>
3723
        </div>
3724
      ) : (
3725
        <div className="agentActions agentManagementActions">
3726
          <button
3727
            className="secondary dangerButton"
3728
            type="button"
3729
            disabled={working || runtimeWorking || deleteWorking}
3730
            onClick={onDelete}
3731
          >
3732
            {deleteWorking ? 'Deleting…' : 'Delete agent'}
3733
          </button>
3734
          <button
3735
            className="secondary"
3736
            type="button"
3737
            disabled={working || runtimeWorking || deleteWorking || agent.status === 'revoked'}
3738
            onClick={() => setEditing(true)}
3739
          >
3740
            Configure automation
3741
          </button>
3742
        </div>
3743
      )}
3744
    </article>
3745
  );
3746
}
3747
function Profile({
3748
  profile,
3749
  expanded,
3750
  deltas = [],
3751
  showHeader = true,
3752
}: {
3753
  profile: Record<string, any>;
3754
  expanded?: boolean;
3755
  deltas?: Record<string, any>[];
3756
  showHeader?: boolean;
3757
}) {
3758
  const dimensions = [
3759
    ['Completion', profile.completion],
3760
    ['Acceptance', profile.acceptance],
3761
    ['Specification', profile.specification],
3762
    ['Scope', profile.scope],
3763
    ['Evidence', profile.evidence],
3764
    ...(expanded
3765
      ? [
3766
          ['Communication', profile.communication],
3767
          ['Deadline', profile.deadline],
3768
          ['Dispute-free', 1 - Number(profile.disputes ?? 0)],
3769
        ]
3770
      : []),
3771
  ] as [string, number][];
3772
  const latestDelta = deltas[0];
3773
  return (
3774
    <article className="profile">
3775
      <div className={showHeader ? 'profileHead' : 'profileHead profileHeadCompact'}>
3776
        {showHeader ? (
3777
          <div>
3778
            <strong>{profile.agentId}</strong>
3779
            <small>
3780
              {humanize(profile.taskCategory)} · version {profile.agentVersion ?? 'unknown'}
3781
            </small>
3782
          </div>
3783
        ) : (
3784
          <small>
3785
            Effective evidence{' '}
3786
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
3787
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
3788
          </small>
3789
        )}
3790
        <span className="sampleBadge">
3791
          {profile.sampleSize ?? 0} outcome{Number(profile.sampleSize) === 1 ? '' : 's'}
3792
        </span>
3793
      </div>
3794
      {showHeader ? (
3795
        <div className="profileEvidenceLine">
3796
          <small>
3797
            Effective evidence{' '}
3798
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
3799
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
3800
          </small>
3801
        </div>
3802
      ) : null}
3803
      {dimensions.map(([label, value]) => (
3804
        <Meter key={label} label={label} value={Number(value ?? 0.5)} />
3805
      ))}
3806
      {latestDelta ? (
3807
        <div className="profileDelta">
3808
          <span>Latest evidence impact</span>
3809
          <div>
3810
            {Object.entries(latestDelta.dimensionDeltas ?? {})
3811
              .filter(([, value]) => Math.abs(Number(value)) >= 0.001)
3812
              .slice(0, 5)
3813
              .map(([dimension, value]) => (
3814
                <b className={Number(value) >= 0 ? 'positive' : 'negative'} key={dimension}>
3815
                  {humanize(dimension)} {Number(value) >= 0 ? '+' : ''}
3816
                  {(Number(value) * 100).toFixed(1)}
3817
                </b>
3818
              ))}
3819
          </div>
3820
        </div>
3821
      ) : null}
3822
    </article>
3823
  );
3824
}
3825
function Meter({ label, value }: { label: string; value: number }) {
3826
  const bounded = Math.max(0, Math.min(1, value));
3827
  return (
3828
    <div className="meter">
3829
      <span>{label}</span>
3830
      <i>
3831
        <b style={{ width: `${Math.round(bounded * 100)}%` }} />
3832
      </i>
3833
      <strong>{Math.round(bounded * 100)}%</strong>
3834
    </div>
3835
  );
3836
}
3837
function Empty({
3838
  title,
3839
  text,
3840
  action,
3841
  onAction,
3842
}: {
3843
  title: string;
3844
  text: string;
3845
  action?: string;
3846
  onAction?: () => void;
3847
}) {
3848
  return (
3849
    <div className="empty">
3850
      <span aria-hidden="true">+</span>
3851
      <strong>{title}</strong>
3852
      <p>{text}</p>
3853
      {action && (
3854
        <button className="secondary" type="button" onClick={onAction}>
3855
          {action}
3856
        </button>
3857
      )}
3858
    </div>
3859
  );
3860
}
3861
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
3862
  return (
3863
    <button className="textButton" type="button" onClick={onClick}>
3864
      {children}
3865
    </button>
3866
  );
3867
}
3868
function Setting({
3869
  label,
3870
  description,
3871
  children,
3872
}: React.PropsWithChildren<{ label: string; description: string }>) {
3873
  return (
3874
    <div className="setting">
3875
      <div>
3876
        <strong>{label}</strong>
3877
        <p>{description}</p>
3878
      </div>
3879
      <div>{children}</div>
3880
    </div>
3881
  );
3882
}
3883
function Toggle({
3884
  checked,
3885
  onChange,
3886
  label,
3887
}: {
3888
  checked: boolean;
3889
  onChange: (checked: boolean) => void;
3890
  label?: string;
3891
}) {
3892
  return (
3893
    <button
3894
      className={checked ? 'toggle on' : 'toggle'}
3895
      type="button"
3896
      role="switch"
3897
      aria-checked={checked}
3898
      aria-label={label}
3899
      onClick={() => onChange(!checked)}
3900
    >
3901
      <span />
3902
    </button>
3903
  );
3904
}
3905
function Loading({ compact }: { compact?: boolean }) {
3906
  return (
3907
    <div className={compact ? 'loading compact' : 'loading'}>
3908
      <span className="spinner" aria-hidden="true" />
3909
      <p>Verifying session…</p>
3910
    </div>
3911
  );
3912
}
3913
3914
function GoogleMark() {
3915
  return (
3916
    <svg viewBox="0 0 24 24" aria-hidden="true">
3917
      <path
3918
        fill="#4285F4"
3919
        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"
3920
      />
3921
      <path
3922
        fill="#34A853"
3923
        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"
3924
      />
3925
      <path
3926
        fill="#FBBC05"
3927
        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"
3928
      />
3929
      <path
3930
        fill="#EA4335"
3931
        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"
3932
      />
3933
    </svg>
3934
  );
3935
}
3936
3937
function GitHubMark() {
3938
  return (
3939
    <svg viewBox="0 0 24 24" aria-hidden="true">
3940
      <path
3941
        fill="currentColor"
3942
        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"
3943
      />
3944
    </svg>
3945
  );
3946
}
3947
3948
function humanize(value: unknown) {
3949
  const text = String(value ?? '')
3950
    .replaceAll('_', ' ')
3951
    .trim();
3952
  return text ? text[0]!.toUpperCase() + text.slice(1) : 'Unknown';
3953
}
3954
3955
function formatDate(value: string) {
3956
  const date = new Date(value);
3957
  return Number.isNaN(date.getTime()) ? 'Time unavailable' : date.toLocaleString();
3958
}
3959
3960
function relativeTime(value: string) {
3961
  const milliseconds = Date.now() - Date.parse(value);
3962
  if (!Number.isFinite(milliseconds)) return 'Time unavailable';
3963
  const minutes = Math.round(Math.abs(milliseconds) / 60_000);
3964
  const future = milliseconds < 0;
3965
  const phrase =
3966
    minutes < 1
3967
      ? 'just now'
3968
      : minutes < 60
3969
        ? `${minutes}m`
3970
        : minutes < 1_440
3971
          ? `${Math.round(minutes / 60)}h`
3972
          : `${Math.round(minutes / 1_440)}d`;
3973
  return minutes < 1 ? phrase : future ? `in ${phrase}` : `${phrase} ago`;
3974
}
3975
3976
function feedbackState(requests: Record<string, any>[]) {
3977
  if (!requests.length) return 'Starts after a completion report';
3978
  const pending = requests.filter((request) => request.status === 'pending').length;
3979
  if (pending) return `${pending} response${pending === 1 ? '' : 's'} still pending`;
3980
  const expired = requests.filter((request) => request.status === 'expired').length;
3981
  return expired
3982
    ? `${expired} request${expired === 1 ? '' : 's'} timed out`
3983
    : 'Bilateral feedback closed';
3984
}
3985
3986
function StatusPill({ value }: { value: string }) {
3987
  const normalized = String(value).toLowerCase();
3988
  const tone = [
3989
    'success',
3990
    'active',
3991
    'ready',
3992
    'met',
3993
    'eligible',
3994
    'attested',
3995
    'allow',
3996
    'completed',
3997
  ].includes(normalized)
3998
    ? 'good'
3999
    : ['failure', 'denied', 'deny', 'missed', 'rejected', 'violation'].some((item) =>
4000
          normalized.includes(item),
4001
        )
4002
      ? 'danger'
4003
      : ['partial', 'pending', 'provisional', 'challenge', 'conflict', 'expired', 'excluded'].some(
4004
            (item) => normalized.includes(item),
4005
          )
4006
        ? 'warn'
4007
        : 'neutral';
4008
  return (
4009
    <span className={`statusPill ${tone}`}>
4010
      <i />
4011
      {humanize(value)}
4012
    </span>
4013
  );
4014
}
4015
4016
function timestamp(item: Record<string, any>) {
4017
  return String(
4018
    item.timestamp ??
4019
      item.generatedAt ??
4020
      item.decidedAt ??
4021
      item.appliedAt ??
4022
      item.submittedAt ??
4023
      item.completedAt ??
4024
      item.activatedAt ??
4025
      item.updatedAt ??
4026
      item.requestedAt ??
4027
      item.createdAt ??
4028
      new Date(0).toISOString(),
4029
  );
4030
}
4031
4032
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
4033
  throw new Error('Auth0 public configuration is incomplete');
4034
createRoot(document.getElementById('root')!).render(
4035
  <React.StrictMode>
4036
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
4037
  </React.StrictMode>,
4038
);