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

Viewing files at commit 578449aaa53eCommit detailsView latest

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