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

Viewing files at commit 1efaca5a5f5eCommit detailsView latest

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