@nxisai/agent-harness 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/handoff/tools.d.ts.map +1 -1
- package/dist/handoff/tools.js +11 -4
- package/dist/handoff/tools.js.map +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +26 -8
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/renderer.d.ts +20 -0
- package/dist/harness/renderer.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.d.ts +29 -0
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +64 -4
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/site-actions.d.ts +27 -0
- package/dist/harness/tools/site-actions.d.ts.map +1 -1
- package/dist/harness/tools/site-actions.js +47 -0
- package/dist/harness/tools/site-actions.js.map +1 -1
- package/dist/harness/tools/source.d.ts.map +1 -1
- package/dist/harness/tools/source.js +5 -2
- package/dist/harness/tools/source.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/action-card.d.ts +141 -0
- package/dist/ui/action-card.d.ts.map +1 -0
- package/dist/ui/action-card.js +384 -0
- package/dist/ui/action-card.js.map +1 -0
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +531 -16
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/tailwind.generated.d.ts +1 -1
- package/dist/ui/tailwind.generated.d.ts.map +1 -1
- package/dist/ui/tailwind.generated.js +1 -1
- package/dist/ui/tailwind.generated.js.map +1 -1
- package/package.json +1 -1
- package/src/handoff/tools.ts +11 -7
- package/src/harness/harness.ts +31 -15
- package/src/harness/renderer.ts +22 -0
- package/src/harness/tools/action-tools.ts +74 -4
- package/src/harness/tools/site-actions.ts +58 -0
- package/src/harness/tools/source.ts +5 -2
- package/src/index.ts +1 -1
- package/src/ui/action-card.ts +496 -0
- package/src/ui/agent-fullscreen.ts +563 -15
- package/src/ui/tailwind.generated.ts +1 -1
|
@@ -41,16 +41,29 @@ import {
|
|
|
41
41
|
import type { ModelProvider } from '../providers/types.js';
|
|
42
42
|
import { connectTools } from '../harness/tools/source.js';
|
|
43
43
|
import {
|
|
44
|
+
chooserOf,
|
|
45
|
+
formOptions,
|
|
44
46
|
handoffFor,
|
|
45
47
|
isSiteTool,
|
|
46
48
|
withoutSiteSuffix,
|
|
47
49
|
HANDOFF_RESULT,
|
|
48
50
|
} from '../harness/tools/action-tools.js';
|
|
51
|
+
import { resolveSlots, type Slot } from '../harness/slots.js';
|
|
52
|
+
import { isHandoffTool } from '../handoff/tools.js';
|
|
53
|
+
import {
|
|
54
|
+
actionCard,
|
|
55
|
+
fieldLabel,
|
|
56
|
+
fieldsFromSlots,
|
|
57
|
+
ACTION_CARD_CSS,
|
|
58
|
+
type ActionHandlers,
|
|
59
|
+
type ActionItem,
|
|
60
|
+
type ActionStep,
|
|
61
|
+
} from './action-card.js';
|
|
49
62
|
import { resumePendingAction } from '../harness/tools/resume.js';
|
|
50
63
|
import { savePendingVisit, setConversationSource, takePendingVisit } from '../harness/tools/pending-action.js';
|
|
51
64
|
import { pageForEntity, type SitePage } from '../harness/tools/site-pages.js';
|
|
52
65
|
import { OPEN_PAGE_TOOL } from '../harness/tools/open-page.js';
|
|
53
|
-
import { onJourneyChange, recordStep, restoreJourney, type AgentStep } from '../harness/steps.js';
|
|
66
|
+
import { currentJourney, onJourneyChange, recordStep, restoreJourney, type AgentStep } from '../harness/steps.js';
|
|
54
67
|
import { viewJourney } from './journey.js';
|
|
55
68
|
import { AgentCoordinator } from '../harness/router/coordinator.js';
|
|
56
69
|
import {
|
|
@@ -59,6 +72,7 @@ import {
|
|
|
59
72
|
type HandoffConfig,
|
|
60
73
|
type HandoffEvent,
|
|
61
74
|
type HandoffSession,
|
|
75
|
+
type HandoffTarget,
|
|
62
76
|
} from '../handoff/controller.js';
|
|
63
77
|
import { registerHandoffTools, HANDOFF_SESSION_RESULT } from '../handoff/tools.js';
|
|
64
78
|
import type { RouteRule } from '../harness/router/rules.js';
|
|
@@ -151,6 +165,12 @@ const fmt = (n: number) => n.toLocaleString();
|
|
|
151
165
|
* be disabled under `prefers-reduced-motion` while the bubble still reads as
|
|
152
166
|
* pending. The label and byte spans are filled per phase and hidden while empty.
|
|
153
167
|
*/
|
|
168
|
+
/**
|
|
169
|
+
* How long the running-step line stays once the conversation moves on past
|
|
170
|
+
* its card. A glance's worth: long enough to notice and open it.
|
|
171
|
+
*/
|
|
172
|
+
const JSTATUS_TIMEOUT_MS = 10_000;
|
|
173
|
+
|
|
154
174
|
const THINKING_MARKUP =
|
|
155
175
|
'<span class="think-row">' +
|
|
156
176
|
'<span class="think-dots" aria-hidden="true"><i></i><i></i><i></i></span>' +
|
|
@@ -331,7 +351,9 @@ type ThreadItem =
|
|
|
331
351
|
/** Something a handoff target said: an agent, or a person at a desk. */
|
|
332
352
|
| { kind: 'remote'; from: string; fromKind: 'agent' | 'person'; text: string }
|
|
333
353
|
/** Where a handoff stands, as a quiet line in the thread. */
|
|
334
|
-
| { kind: 'handoff'; text: string; tone: 'info' | 'done' | 'error' }
|
|
354
|
+
| { kind: 'handoff'; text: string; tone: 'info' | 'done' | 'error' }
|
|
355
|
+
/** A form or a handoff, asked for, confirmed and sent in one card. See ./action-card.ts. */
|
|
356
|
+
| ActionItem;
|
|
335
357
|
|
|
336
358
|
/**
|
|
337
359
|
* A handoff's state as a line in the thread, or '' for a state the bar alone
|
|
@@ -457,6 +479,12 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
457
479
|
};
|
|
458
480
|
/** Watches whether the running journey's card is on screen. */
|
|
459
481
|
private _journeyObserver: IntersectionObserver | null = null;
|
|
482
|
+
/** The journey the status line is about, while one is running. */
|
|
483
|
+
private _jstatusJourney: string | null = null;
|
|
484
|
+
/** Journeys whose status line was opened once or timed out: it does not come back. */
|
|
485
|
+
private _jstatusDismissed = new Set<string>();
|
|
486
|
+
/** Dismisses the status line a while after the thread moves on past the card. */
|
|
487
|
+
private _jstatusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
460
488
|
/** The turn loop. This element is only its renderer. */
|
|
461
489
|
private _harness!: AgentHarness;
|
|
462
490
|
/** Built only when the host defines agents — otherwise there is one harness. */
|
|
@@ -466,6 +494,22 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
466
494
|
private _fallbackAgent = '';
|
|
467
495
|
/** The live ask/confirm surface, if one is open. */
|
|
468
496
|
private _prompt: MountedSurface | null = null;
|
|
497
|
+
/**
|
|
498
|
+
* The action card still waiting on the visitor or a result, and what it can
|
|
499
|
+
* do: the harness's ask or confirm request, or, after a page change, a call
|
|
500
|
+
* of the widget's own.
|
|
501
|
+
*/
|
|
502
|
+
private _live: {
|
|
503
|
+
item: ActionItem;
|
|
504
|
+
tool: ModelContextToolInfo;
|
|
505
|
+
ask?: AskRequest;
|
|
506
|
+
confirm?: ConfirmRequest;
|
|
507
|
+
run?: (values: Record<string, unknown>) => Promise<void>;
|
|
508
|
+
sent: boolean;
|
|
509
|
+
declining: boolean;
|
|
510
|
+
} | null = null;
|
|
511
|
+
/** Handoff targets as the relay last reported them, for a card's availability line. */
|
|
512
|
+
private _handoffTargets = new Map<string, HandoffTarget>();
|
|
469
513
|
private _profile: Record<string, string>;
|
|
470
514
|
private _persistPrefs = false;
|
|
471
515
|
private _systemPrompt: string = DEFAULT_SYSTEM_PROMPT;
|
|
@@ -579,13 +623,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
579
623
|
|
|
580
624
|
// The element renders; the harness decides. Everything from choosing a
|
|
581
625
|
// tool to running it happens behind harness.turn().
|
|
582
|
-
this._harness =
|
|
583
|
-
provider: this.provider,
|
|
584
|
-
toolSource: this.toolSource,
|
|
585
|
-
renderer: this._renderer(),
|
|
586
|
-
profile: (schema) => this._profileForSchema(schema as JsonSchema),
|
|
587
|
-
onMessages: () => this._saveConversations(),
|
|
588
|
-
});
|
|
626
|
+
this._harness = this._singleHarness();
|
|
589
627
|
|
|
590
628
|
// Preferences are supplied by the host page through `element.preferences`.
|
|
591
629
|
// Anything a previous build persisted is picked up so upgrading does not
|
|
@@ -620,6 +658,8 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
620
658
|
this._els.jstatus.addEventListener('click', () => {
|
|
621
659
|
const cards = this._els.thread.querySelectorAll<HTMLElement>('.journey.is-running');
|
|
622
660
|
cards[cards.length - 1]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
661
|
+
// One look is what it is for; after that it would only be in the way.
|
|
662
|
+
this._dismissJourneyStatus();
|
|
623
663
|
});
|
|
624
664
|
|
|
625
665
|
// Whoever builds the tools, a call that leaves this page reports back to
|
|
@@ -715,6 +755,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
715
755
|
this._offJourney = null;
|
|
716
756
|
this._journeyObserver?.disconnect();
|
|
717
757
|
this._journeyObserver = null;
|
|
758
|
+
this._clearJourneyStatusTimer();
|
|
718
759
|
// Streams stop with the element; the handoff itself stays saved, and resumes when it returns.
|
|
719
760
|
this._handoff?.dispose();
|
|
720
761
|
}
|
|
@@ -903,7 +944,10 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
903
944
|
else if (it.kind === 'detail') this._els.thread.appendChild(this._detailEl(it));
|
|
904
945
|
else if (it.kind === 'results') this._els.thread.appendChild(this._resultsEl(it));
|
|
905
946
|
else if (it.kind === 'metrics') this._els.thread.appendChild(this._metricsEl(it));
|
|
906
|
-
else if (it.kind === 'journey')
|
|
947
|
+
else if (it.kind === 'journey') {
|
|
948
|
+
// A request drawn as an action card shows its own steps.
|
|
949
|
+
if (!conv.items.some((a) => a.kind === 'action' && a.id === it.id)) this._els.thread.appendChild(this._journeyEl(it));
|
|
950
|
+
} else if (it.kind === 'action') this._paintAction(it);
|
|
907
951
|
else if (it.kind === 'remote' || it.kind === 'handoff') this._paintHandoffItem(it);
|
|
908
952
|
}
|
|
909
953
|
this._scroll();
|
|
@@ -1288,6 +1332,15 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1288
1332
|
conv.items.push(item);
|
|
1289
1333
|
}
|
|
1290
1334
|
|
|
1335
|
+
// A request with an action card: the steps belong in the card.
|
|
1336
|
+
const action = conv.items.find((it): it is ActionItem => it.kind === 'action' && it.id === view.id);
|
|
1337
|
+
if (action) {
|
|
1338
|
+
this._actionFollows(action, all);
|
|
1339
|
+
this._watchRunningJourney();
|
|
1340
|
+
this._saveConversations();
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1291
1344
|
if (conv.id === this._activeId && this._els?.thread) {
|
|
1292
1345
|
const fresh = this._journeyEl(item);
|
|
1293
1346
|
const old = this._els.thread.querySelector(`[data-journey="${CSS.escape(view.id)}"]`);
|
|
@@ -1319,7 +1372,12 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1319
1372
|
if (!bar) return;
|
|
1320
1373
|
const cards = this._els.thread.querySelectorAll<HTMLElement>('.journey.is-running');
|
|
1321
1374
|
const card = cards[cards.length - 1];
|
|
1322
|
-
|
|
1375
|
+
const id = card?.dataset.journey ?? null;
|
|
1376
|
+
if (id !== this._jstatusJourney) {
|
|
1377
|
+
this._clearJourneyStatusTimer();
|
|
1378
|
+
this._jstatusJourney = id;
|
|
1379
|
+
}
|
|
1380
|
+
if (!card || !id || this._jstatusDismissed.has(id) || typeof IntersectionObserver === 'undefined') {
|
|
1323
1381
|
bar.hidden = true;
|
|
1324
1382
|
return;
|
|
1325
1383
|
}
|
|
@@ -1333,6 +1391,36 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1333
1391
|
this._journeyObserver.observe(card);
|
|
1334
1392
|
}
|
|
1335
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* The thread grew: start the clock on the status line.
|
|
1396
|
+
*
|
|
1397
|
+
* A journey can stay running while the conversation goes on without it, and a
|
|
1398
|
+
* line that spins through all of that says nothing. So once new messages
|
|
1399
|
+
* arrive, the line gets a while longer, then goes.
|
|
1400
|
+
*/
|
|
1401
|
+
private _noteThreadGrew(): void {
|
|
1402
|
+
const id = this._jstatusJourney;
|
|
1403
|
+
if (!id || this._jstatusTimer || this._jstatusDismissed.has(id)) return;
|
|
1404
|
+
this._jstatusTimer = setTimeout(() => {
|
|
1405
|
+
this._jstatusTimer = null;
|
|
1406
|
+
this._dismissJourneyStatus();
|
|
1407
|
+
}, JSTATUS_TIMEOUT_MS);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Hide the status line for the running journey, for good. */
|
|
1411
|
+
private _dismissJourneyStatus(): void {
|
|
1412
|
+
this._clearJourneyStatusTimer();
|
|
1413
|
+
if (this._jstatusJourney) this._jstatusDismissed.add(this._jstatusJourney);
|
|
1414
|
+
this._journeyObserver?.disconnect();
|
|
1415
|
+
this._journeyObserver = null;
|
|
1416
|
+
if (this._els?.jstatus) this._els.jstatus.hidden = true;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
private _clearJourneyStatusTimer(): void {
|
|
1420
|
+
if (this._jstatusTimer) clearTimeout(this._jstatusTimer);
|
|
1421
|
+
this._jstatusTimer = null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1336
1424
|
private _paintToolResult(name: string, text: string, isError: boolean): HTMLElement {
|
|
1337
1425
|
this._clearEmpty();
|
|
1338
1426
|
const el = this._toolEl(name, text, isError);
|
|
@@ -1391,6 +1479,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1391
1479
|
const el = this._paintMsg(role, text, conv.items.length - 1);
|
|
1392
1480
|
this._saveConversations();
|
|
1393
1481
|
this._scroll();
|
|
1482
|
+
this._noteThreadGrew();
|
|
1394
1483
|
return el;
|
|
1395
1484
|
}
|
|
1396
1485
|
|
|
@@ -1411,6 +1500,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1411
1500
|
// without the focus moving to it.
|
|
1412
1501
|
el.setAttribute('aria-live', 'polite');
|
|
1413
1502
|
this._scroll();
|
|
1503
|
+
this._noteThreadGrew();
|
|
1414
1504
|
|
|
1415
1505
|
const setLabel = (label?: string) => {
|
|
1416
1506
|
const node = el.querySelector('.think-label');
|
|
@@ -1552,8 +1642,10 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1552
1642
|
message: (role, text) => this._appendMsg(role, text),
|
|
1553
1643
|
streaming: (): HStreamingBubble => this._appendStreaming(),
|
|
1554
1644
|
status: (text, isError) => this._setStatus(text, isError),
|
|
1555
|
-
|
|
1556
|
-
|
|
1645
|
+
inlinePrompts: !this._classicPrompts,
|
|
1646
|
+
ask: (req) => (this._usesCard(req.tool) ? this._cardAsk(req) : this._renderAsk(req)),
|
|
1647
|
+
confirm: (req) => (this._usesCard(req.tool) ? this._cardConfirm(req) : this._renderConfirm(req)),
|
|
1648
|
+
left: (tool, destination) => this._cardLeft(tool, destination),
|
|
1557
1649
|
results: (tool, res) => {
|
|
1558
1650
|
const conv = this._active();
|
|
1559
1651
|
const item = this._buildResultsItem(tool, res);
|
|
@@ -1575,6 +1667,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1575
1667
|
this._scroll();
|
|
1576
1668
|
},
|
|
1577
1669
|
detail: (tool, values, text) => {
|
|
1670
|
+
if (this._cardDone(tool, values, text)) return;
|
|
1578
1671
|
const conv = this._active();
|
|
1579
1672
|
const item = this._buildDetailItem(tool, values, text);
|
|
1580
1673
|
conv?.items.push(item);
|
|
@@ -1598,8 +1691,23 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1598
1691
|
this._saveConversations();
|
|
1599
1692
|
this._scroll();
|
|
1600
1693
|
},
|
|
1601
|
-
toolError: (tool, message) =>
|
|
1602
|
-
|
|
1694
|
+
toolError: (tool, message) => {
|
|
1695
|
+
if (this._cardFailed(tool, message)) return;
|
|
1696
|
+
this._paintToolResult(tool.name, message, true);
|
|
1697
|
+
},
|
|
1698
|
+
dismissPrompt: () => {
|
|
1699
|
+
// A sent card stays, and shows the steps; one never sent says so.
|
|
1700
|
+
const live = this._live;
|
|
1701
|
+
if (live) {
|
|
1702
|
+
if (live.sent && (live.item.stage === 'review' || live.item.stage === 'collect')) {
|
|
1703
|
+
live.item.stage = 'working';
|
|
1704
|
+
this._paintAction(live.item);
|
|
1705
|
+
this._saveConversations();
|
|
1706
|
+
}
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
this._dismissPrompt();
|
|
1710
|
+
},
|
|
1603
1711
|
// The harness says when stopping stops being possible, because it is the
|
|
1604
1712
|
// part that knows a tool has been invoked. Once a call is out, offering
|
|
1605
1713
|
// to stop would be offering to un-send a booking.
|
|
@@ -1748,6 +1856,390 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1748
1856
|
this._scroll();
|
|
1749
1857
|
}
|
|
1750
1858
|
|
|
1859
|
+
// ── Action cards ─────────────────────────────────────────────────────────────
|
|
1860
|
+
|
|
1861
|
+
/** `prompt-style="classic"`: the separate ask, review and submitted cards of earlier versions. */
|
|
1862
|
+
private get _classicPrompts(): boolean {
|
|
1863
|
+
return this.getAttribute('prompt-style') === 'classic';
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** Whether a prompt for this tool is drawn as an action card: every write and handoff. */
|
|
1867
|
+
private _usesCard(tool: ModelContextToolInfo): boolean {
|
|
1868
|
+
return !this._classicPrompts && (isHandoffTool(tool.name) || !tool.annotations?.readOnlyHint);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/** Details are missing: the card asks for them, beside what it already has. */
|
|
1872
|
+
private _cardAsk(req: AskRequest): void {
|
|
1873
|
+
const needed = new Set(req.plan.steps.flatMap((st) => st.slots.map((sl) => sl.name)));
|
|
1874
|
+
const item = this._liveCard(req.tool, req.resolution.all, req.resolution.values, needed);
|
|
1875
|
+
this._live!.ask = req;
|
|
1876
|
+
this._live!.confirm = undefined;
|
|
1877
|
+
item.stage = 'collect';
|
|
1878
|
+
this._paintAction(item);
|
|
1879
|
+
this._saveConversations();
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/** Everything is known: the card says what will happen, and waits for Send. */
|
|
1883
|
+
private _cardConfirm(req: ConfirmRequest): void {
|
|
1884
|
+
const all = resolveSlots(toolSchema(req.tool), { modelArgs: req.values }).all;
|
|
1885
|
+
const item = this._liveCard(req.tool, all, req.values, new Set(), req.destination);
|
|
1886
|
+
this._live!.confirm = req;
|
|
1887
|
+
item.stage = 'review';
|
|
1888
|
+
this._paintAction(item);
|
|
1889
|
+
this._saveConversations();
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* The live card for a tool's prompt: the one already open for this request,
|
|
1894
|
+
* or a new one, which takes over from the journey card of the same request.
|
|
1895
|
+
*/
|
|
1896
|
+
private _liveCard(
|
|
1897
|
+
tool: ModelContextToolInfo,
|
|
1898
|
+
slots: Slot[],
|
|
1899
|
+
values: Record<string, unknown>,
|
|
1900
|
+
needed: Set<string>,
|
|
1901
|
+
destination?: string,
|
|
1902
|
+
): ActionItem {
|
|
1903
|
+
const conv = this._active()!;
|
|
1904
|
+
const id = currentJourney() ?? `a${Date.now().toString(36)}`;
|
|
1905
|
+
const live = this._live;
|
|
1906
|
+
const described = this._describeAction(tool, values, destination);
|
|
1907
|
+
const fields = fieldsFromSlots(this._narrowed(tool, slots, values), values, { needed, hidden: described.hidden, labels: described.labels });
|
|
1908
|
+
|
|
1909
|
+
if (live && live.item.id === id && live.tool.name === tool.name) {
|
|
1910
|
+
// The same request, a step on: keep what was typed, and anything the harness has since learned.
|
|
1911
|
+
live.item.fields = fields.map((f) => {
|
|
1912
|
+
const had = live.item.fields.find((o) => o.name === f.name);
|
|
1913
|
+
return had && f.value === '' ? { ...f, value: had.value } : f;
|
|
1914
|
+
});
|
|
1915
|
+
Object.assign(live.item, described.view);
|
|
1916
|
+
live.tool = tool;
|
|
1917
|
+
return live.item;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// A request left waiting is replaced by the new one.
|
|
1921
|
+
if (live && (live.item.stage === 'collect' || live.item.stage === 'review')) {
|
|
1922
|
+
live.item.stage = 'declined';
|
|
1923
|
+
this._paintAction(live.item);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const item: ActionItem = { kind: 'action', id, tool: tool.name, fields, stage: 'collect', ...described.view };
|
|
1927
|
+
conv.items.push(item);
|
|
1928
|
+
this._live = { item, tool, sent: false, declining: false };
|
|
1929
|
+
this._els?.thread?.querySelector(`[data-journey="${CSS.escape(id)}"]`)?.remove();
|
|
1930
|
+
this._watchRunningJourney();
|
|
1931
|
+
if (isHandoffTool(tool.name)) void this._refreshHandoffTargets(item);
|
|
1932
|
+
return item;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* The slots with each field's options cut to the form the call means: one
|
|
1937
|
+
* business's services rather than every business's.
|
|
1938
|
+
*/
|
|
1939
|
+
private _narrowed(tool: ModelContextToolInfo, slots: Slot[], values: Record<string, unknown>): Slot[] {
|
|
1940
|
+
const options = formOptions(tool, values);
|
|
1941
|
+
return slots.map((slot) => {
|
|
1942
|
+
const only = options[slot.name];
|
|
1943
|
+
if (!only) return slot;
|
|
1944
|
+
// Keep a value the call already gave, even one the form does not list: the form decides.
|
|
1945
|
+
const current = values[slot.name];
|
|
1946
|
+
const list = current !== undefined && current !== '' && !only.includes(String(current)) ? [String(current), ...only] : only;
|
|
1947
|
+
return { ...slot, schema: { ...slot.schema, enum: list } };
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/** Who a request goes to, and how the card words it. */
|
|
1952
|
+
private _describeAction(
|
|
1953
|
+
tool: ModelContextToolInfo,
|
|
1954
|
+
values: Record<string, unknown>,
|
|
1955
|
+
destination?: string,
|
|
1956
|
+
): {
|
|
1957
|
+
view: Pick<ActionItem, 'variant' | 'noun' | 'identity' | 'sentence' | 'note' | 'sendLabel'>;
|
|
1958
|
+
hidden: Set<string>;
|
|
1959
|
+
labels: Record<string, string>;
|
|
1960
|
+
} {
|
|
1961
|
+
const email = String(values.email ?? Object.entries(values).find(([k]) => /e-?mail/i.test(k))?.[1] ?? '').trim();
|
|
1962
|
+
|
|
1963
|
+
if (isHandoffTool(tool.name)) {
|
|
1964
|
+
const name = String(values.target ?? '');
|
|
1965
|
+
const config = this._handoff?.config?.targets[name];
|
|
1966
|
+
const known = this._handoffTargets.get(name);
|
|
1967
|
+
const label = known?.label ?? config?.label ?? fieldLabel(name || 'the team');
|
|
1968
|
+
const person = (known?.kind ?? config?.kind) !== 'agent';
|
|
1969
|
+
const away = known ? !known.available : false;
|
|
1970
|
+
// "The directory team" leads a line, but mid-sentence it is "the directory team".
|
|
1971
|
+
const inline = label.replace(/^The\s/, 'the ');
|
|
1972
|
+
const about = known?.description ?? config?.description;
|
|
1973
|
+
const status = !known
|
|
1974
|
+
? undefined
|
|
1975
|
+
: away
|
|
1976
|
+
? `Away now${known.hours ? ` · open ${known.hours}` : ''}`
|
|
1977
|
+
: person
|
|
1978
|
+
? known.waitMinutes
|
|
1979
|
+
? `Available · replies in ~${known.waitMinutes} min`
|
|
1980
|
+
: 'Available'
|
|
1981
|
+
: 'Available';
|
|
1982
|
+
const sentence = !person
|
|
1983
|
+
? `I'll hand this conversation to ${inline}. They'll take it from here.`
|
|
1984
|
+
: away
|
|
1985
|
+
? email
|
|
1986
|
+
? `${label} is away right now. They'll reply to ${email}.`
|
|
1987
|
+
: `${label} is away right now. Leave an email and they'll reply there.`
|
|
1988
|
+
: `I'll pass this to ${inline} so you don't have to repeat yourself.`;
|
|
1989
|
+
return {
|
|
1990
|
+
view: {
|
|
1991
|
+
variant: person ? 'handoff-person' : 'handoff-agent',
|
|
1992
|
+
noun: person ? 'handoff' : 'handover',
|
|
1993
|
+
identity: { name: label, sub: about ? about.charAt(0).toUpperCase() + about.slice(1) : undefined, status, tone: away ? 'away' : 'ok' },
|
|
1994
|
+
sentence,
|
|
1995
|
+
note: undefined,
|
|
1996
|
+
sendLabel: !person ? 'Hand over' : away ? `Send to ${inline}` : 'Connect me',
|
|
1997
|
+
},
|
|
1998
|
+
hidden: new Set(['target']),
|
|
1999
|
+
labels: { reason: 'Why', summary: 'What they need to know', email: 'Where should they reply?' },
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
const noun = fieldLabel(withoutSiteSuffix(tool.name)).toLowerCase();
|
|
2004
|
+
// Which business or place it is for: the site tool's own chooser, else a field the form names it in.
|
|
2005
|
+
const chooser = chooserOf(tool) ?? Object.keys(values).find((k) => /^(business|company|organi[sz]ation|provider|vendor|store|restaurant|practice|venue)(_name)?$/i.test(k)) ?? null;
|
|
2006
|
+
const chosen = chooser ? String(values[chooser] ?? '').trim() : '';
|
|
2007
|
+
const leaving = destination ?? handoffFor(tool, values)?.label;
|
|
2008
|
+
const name = chosen || leaving || this._pageName();
|
|
2009
|
+
// What the conversation already showed about them: their card from a search.
|
|
2010
|
+
const card = this._conversations
|
|
2011
|
+
.find((c) => c.id === this._activeId)
|
|
2012
|
+
?.items.flatMap((it) => (it.kind === 'results' ? it.cards : []))
|
|
2013
|
+
.find((c) => c.title.toLowerCase() === name.toLowerCase());
|
|
2014
|
+
const sub = [fieldLabel(noun), card?.type && card.type !== 'Thing' ? fieldLabel(card.type) : '', card?.rating ? `★ ${card.rating}` : '']
|
|
2015
|
+
.filter(Boolean)
|
|
2016
|
+
.join(' · ');
|
|
2017
|
+
return {
|
|
2018
|
+
view: {
|
|
2019
|
+
variant: 'form',
|
|
2020
|
+
noun,
|
|
2021
|
+
identity: { name, sub },
|
|
2022
|
+
sentence: `I'll send this ${noun} to ${name}.${email ? ` They'll reply to ${email}.` : ''}`,
|
|
2023
|
+
note: leaving ? 'Their form is on its own page. I’ll open it and send it from there.' : undefined,
|
|
2024
|
+
sendLabel: 'Send',
|
|
2025
|
+
},
|
|
2026
|
+
hidden: new Set(chooser && chooser !== 'target' && chosen ? [chooser] : chooser === 'target' ? ['target'] : []),
|
|
2027
|
+
labels: {},
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** The site's name for this page: its title, without the site's own name after it. */
|
|
2032
|
+
private _pageName(): string {
|
|
2033
|
+
const title = (typeof document === 'undefined' ? '' : document.title).split(/\s+[|—–·-]\s+/)[0].trim();
|
|
2034
|
+
return title || 'this page';
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/** Read availability for a handoff card, and redraw it once known. */
|
|
2038
|
+
private async _refreshHandoffTargets(item: ActionItem): Promise<void> {
|
|
2039
|
+
const targets = await this._handoff?.targets().catch(() => []);
|
|
2040
|
+
for (const t of targets ?? []) this._handoffTargets.set(t.name, t);
|
|
2041
|
+
if (item.stage !== 'collect' && item.stage !== 'review') return;
|
|
2042
|
+
const values = Object.fromEntries(item.fields.map((f) => [f.name, f.value]));
|
|
2043
|
+
Object.assign(item, this._describeAction({ name: item.tool } as ModelContextToolInfo, values).view);
|
|
2044
|
+
this._paintAction(item);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/** What the live card's buttons do. */
|
|
2048
|
+
private _actionHandlers(): ActionHandlers {
|
|
2049
|
+
return {
|
|
2050
|
+
continue: (values) => {
|
|
2051
|
+
const live = this._live;
|
|
2052
|
+
if (!live) return;
|
|
2053
|
+
if (live.ask && !live.confirm) {
|
|
2054
|
+
live.ask.onComplete(values);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
// Back from a change, or a card of the widget's own: review again, here.
|
|
2058
|
+
Object.assign(live.item, this._describeAction(live.tool, values, live.confirm?.destination).view);
|
|
2059
|
+
live.item.stage = 'review';
|
|
2060
|
+
this._paintAction(live.item);
|
|
2061
|
+
this._saveConversations();
|
|
2062
|
+
},
|
|
2063
|
+
change: () => {
|
|
2064
|
+
const live = this._live;
|
|
2065
|
+
if (!live) return;
|
|
2066
|
+
live.item.fields.forEach((f) => {
|
|
2067
|
+
if (f.group === 'needed' && f.value.trim() !== '') f.group = 'known';
|
|
2068
|
+
});
|
|
2069
|
+
live.sent = false;
|
|
2070
|
+
live.item.stage = 'collect';
|
|
2071
|
+
live.item.open = true;
|
|
2072
|
+
this._paintAction(live.item);
|
|
2073
|
+
this._saveConversations();
|
|
2074
|
+
},
|
|
2075
|
+
send: (values) => {
|
|
2076
|
+
const live = this._live;
|
|
2077
|
+
if (!live) return;
|
|
2078
|
+
live.sent = true;
|
|
2079
|
+
if (live.confirm) live.confirm.onSubmit(values);
|
|
2080
|
+
else if (live.run) void live.run(values);
|
|
2081
|
+
},
|
|
2082
|
+
decline: () => {
|
|
2083
|
+
const live = this._live;
|
|
2084
|
+
if (!live) return;
|
|
2085
|
+
live.declining = true;
|
|
2086
|
+
if (live.confirm) live.confirm.onCancel();
|
|
2087
|
+
else if (live.ask) live.ask.onCancel();
|
|
2088
|
+
// A card the harness is not waiting on declines itself.
|
|
2089
|
+
if (this._live === live) {
|
|
2090
|
+
live.item.stage = 'declined';
|
|
2091
|
+
this._paintAction(live.item);
|
|
2092
|
+
this._live = null;
|
|
2093
|
+
this._saveConversations();
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/** Draw a card where it was, or at the end of the thread. */
|
|
2100
|
+
private _paintAction(item: ActionItem): void {
|
|
2101
|
+
if (!this._els?.thread) return;
|
|
2102
|
+
const conv = this._active();
|
|
2103
|
+
if (!conv?.items.includes(item)) return;
|
|
2104
|
+
const live = this._live?.item === item;
|
|
2105
|
+
const fresh = actionCard(item, live ? this._actionHandlers() : null, () => this._saveConversations());
|
|
2106
|
+
const old = this._els.thread.querySelector(`[data-action="${CSS.escape(item.id)}"]`);
|
|
2107
|
+
if (old) old.replaceWith(fresh);
|
|
2108
|
+
else {
|
|
2109
|
+
this._clearEmpty();
|
|
2110
|
+
this._els.thread.appendChild(fresh);
|
|
2111
|
+
this._scroll();
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/** A request's steps, as its card shows them once sent. */
|
|
2116
|
+
private _actionFollows(item: ActionItem, all: AgentStep[]): void {
|
|
2117
|
+
const live = this._live?.item === item ? this._live : null;
|
|
2118
|
+
const failed = [...all].reverse().find((st) => st.status === 'failed');
|
|
2119
|
+
const done = all.some((st) => st.id === 'submit' && st.status === 'done');
|
|
2120
|
+
const running = ['navigate', 'arrive', 'collect', 'fill', 'submit'];
|
|
2121
|
+
item.steps = all
|
|
2122
|
+
.filter((st) => running.includes(st.id))
|
|
2123
|
+
.map((st): ActionStep => ({
|
|
2124
|
+
label:
|
|
2125
|
+
st.id === 'fill' && st.status === 'active' && item.variant === 'form'
|
|
2126
|
+
? `Filling in the ${item.noun}`
|
|
2127
|
+
: st.id === 'submit' && st.status === 'done' && item.variant === 'form'
|
|
2128
|
+
? 'Sent'
|
|
2129
|
+
: st.label,
|
|
2130
|
+
status: st.status === 'failed' ? 'failed' : st.status === 'done' ? 'done' : 'active',
|
|
2131
|
+
}));
|
|
2132
|
+
|
|
2133
|
+
if (failed) {
|
|
2134
|
+
if (live?.declining || failed.label === 'Cancelled') {
|
|
2135
|
+
item.stage = 'declined';
|
|
2136
|
+
if (live) this._live = null;
|
|
2137
|
+
} else {
|
|
2138
|
+
item.stage = 'failed';
|
|
2139
|
+
item.message = failed.detail || failed.label;
|
|
2140
|
+
}
|
|
2141
|
+
} else if (done) {
|
|
2142
|
+
item.stage = 'done';
|
|
2143
|
+
if (live) this._live = null;
|
|
2144
|
+
} else if (item.stage === 'left' || (item.stage === 'working' && item.steps.length)) {
|
|
2145
|
+
item.stage = 'working';
|
|
2146
|
+
}
|
|
2147
|
+
this._paintAction(item);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/** The confirmed call is leaving for its page. The card says so, and the new page finishes it. */
|
|
2151
|
+
private _cardLeft(tool: ModelContextToolInfo, destination: string | null): void {
|
|
2152
|
+
const live = this._live;
|
|
2153
|
+
if (!live || live.tool.name !== tool.name) {
|
|
2154
|
+
this._appendMsg('assistant', destination ? `Opening ${destination}…` : 'Opening the page that can submit this…');
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
live.item.stage = 'left';
|
|
2158
|
+
live.item.message = `Opening ${destination ?? live.item.identity.name}’s page to send it from there.`;
|
|
2159
|
+
this._live = null;
|
|
2160
|
+
this._paintAction(live.item);
|
|
2161
|
+
this._saveConversations();
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/** The form took it: the card becomes the receipt. False when no card is waiting on this tool. */
|
|
2165
|
+
private _cardDone(tool: ModelContextToolInfo, values: Record<string, unknown>, text: string): boolean {
|
|
2166
|
+
const conv = this._active();
|
|
2167
|
+
const id = currentJourney();
|
|
2168
|
+
const item = conv?.items.find((it): it is ActionItem => it.kind === 'action' && it.id === id && it.tool === tool.name);
|
|
2169
|
+
if (!item || this._classicPrompts) return false;
|
|
2170
|
+
const note = this._buildDetailItem(tool, values, text).note;
|
|
2171
|
+
// The default note says nothing the card's "Sent" does not.
|
|
2172
|
+
item.message = /submitted successfully/.test(note) ? undefined : note;
|
|
2173
|
+
item.stage = 'done';
|
|
2174
|
+
item.fields.forEach((f) => {
|
|
2175
|
+
if (values[f.name] !== undefined) f.value = String(values[f.name]);
|
|
2176
|
+
});
|
|
2177
|
+
if (this._live?.item === item) this._live = null;
|
|
2178
|
+
this._paintAction(item);
|
|
2179
|
+
this._saveConversations();
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/** It did not go through: the card says why and offers to change it. False when no card is waiting. */
|
|
2184
|
+
private _cardFailed(tool: ModelContextToolInfo, message: string): boolean {
|
|
2185
|
+
const live = this._live;
|
|
2186
|
+
if (!live || live.tool.name !== tool.name) return false;
|
|
2187
|
+
live.sent = false;
|
|
2188
|
+
live.item.stage = 'failed';
|
|
2189
|
+
live.item.message = message;
|
|
2190
|
+
this._paintAction(live.item);
|
|
2191
|
+
this._saveConversations();
|
|
2192
|
+
return true;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* After a page change: the card of the request that travelled, found by its
|
|
2197
|
+
* journey, now finished or asked about here.
|
|
2198
|
+
*/
|
|
2199
|
+
private _resumedCard(journeyId: string | undefined): ActionItem | null {
|
|
2200
|
+
if (!journeyId || this._classicPrompts) return null;
|
|
2201
|
+
return this._active()?.items.find((it): it is ActionItem => it.kind === 'action' && it.id === journeyId) ?? null;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/** Let a resumed card be changed and sent again from this page, with the page's own tool. */
|
|
2205
|
+
private _reviveCard(item: ActionItem, tool: ModelContextToolInfo, values: Record<string, unknown>, needed: string[]): void {
|
|
2206
|
+
const slots = resolveSlots(toolSchema(tool), { modelArgs: values }).all;
|
|
2207
|
+
const hidden = new Set(item.fields.filter((f) => f.group === 'hidden').map((f) => f.name));
|
|
2208
|
+
item.fields = fieldsFromSlots(slots, values, { needed: new Set(needed), hidden });
|
|
2209
|
+
this._live = {
|
|
2210
|
+
item,
|
|
2211
|
+
tool,
|
|
2212
|
+
sent: false,
|
|
2213
|
+
declining: false,
|
|
2214
|
+
run: async (vals) => {
|
|
2215
|
+
item.stage = 'working';
|
|
2216
|
+
item.steps = [{ label: `Sending the ${item.noun}`, status: 'active' }];
|
|
2217
|
+
this._paintAction(item);
|
|
2218
|
+
try {
|
|
2219
|
+
const res = await this.toolSource.execTool(tool, tool.name, vals);
|
|
2220
|
+
const text = extractResult(res);
|
|
2221
|
+
if (res?.isError) {
|
|
2222
|
+
this._cardFailed(tool, text || 'That form did not accept the details.');
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
this._active()?.messages.push({
|
|
2226
|
+
role: 'user',
|
|
2227
|
+
content: `[${tool.name} submitted with: ${JSON.stringify(vals)}]. Keep these unless I ask to change them.`,
|
|
2228
|
+
});
|
|
2229
|
+
this._live = null;
|
|
2230
|
+
const note = this._buildDetailItem(tool, vals, text).note;
|
|
2231
|
+
item.message = /submitted successfully/.test(note) ? undefined : note;
|
|
2232
|
+
item.stage = 'done';
|
|
2233
|
+
item.steps = [];
|
|
2234
|
+
this._paintAction(item);
|
|
2235
|
+
this._saveConversations();
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
this._cardFailed(tool, String((err as Error)?.message || err));
|
|
2238
|
+
}
|
|
2239
|
+
},
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
|
|
1751
2243
|
// ── Inline generated form (from JSON-Schema) ───────────────────────────────
|
|
1752
2244
|
private _renderForm(tool: ModelContextToolInfo, args: Record<string, unknown>): void {
|
|
1753
2245
|
if (this._useA2ui) {
|
|
@@ -2692,6 +3184,17 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2692
3184
|
* `fallback`. Inexpensive: each agent is a role over the one shared provider,
|
|
2693
3185
|
* so no additional model is loaded.
|
|
2694
3186
|
*/
|
|
3187
|
+
/** The one agent a page without `agents` runs, on the current provider. */
|
|
3188
|
+
private _singleHarness(): AgentHarness {
|
|
3189
|
+
return new AgentHarness({
|
|
3190
|
+
provider: this.provider,
|
|
3191
|
+
toolSource: this.toolSource,
|
|
3192
|
+
renderer: this._renderer(),
|
|
3193
|
+
profile: (schema) => this._profileForSchema(schema as JsonSchema),
|
|
3194
|
+
onMessages: () => this._saveConversations(),
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2695
3198
|
private _buildAgents(): void {
|
|
2696
3199
|
if (!this.provider || !this._agentDefs) {
|
|
2697
3200
|
this._coordinator = null;
|
|
@@ -2842,6 +3345,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2842
3345
|
if (conversationId === this._activeId && this._els?.thread) {
|
|
2843
3346
|
this._paintHandoffItem(item);
|
|
2844
3347
|
this._scroll();
|
|
3348
|
+
this._noteThreadGrew();
|
|
2845
3349
|
}
|
|
2846
3350
|
}
|
|
2847
3351
|
|
|
@@ -2991,6 +3495,44 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2991
3495
|
});
|
|
2992
3496
|
if (outcome.status !== 'none') this._setStatus('');
|
|
2993
3497
|
|
|
3498
|
+
// The card that travelled with the request finishes here, when there is one.
|
|
3499
|
+
const card = outcome.status === 'none' ? null : this._resumedCard(outcome.pending.steps?.find((st) => st.journey)?.journey);
|
|
3500
|
+
if (card && outcome.status !== 'none') {
|
|
3501
|
+
switch (outcome.status) {
|
|
3502
|
+
case 'no-registry':
|
|
3503
|
+
case 'no-tool':
|
|
3504
|
+
card.stage = 'failed';
|
|
3505
|
+
card.message = `I couldn't find the ${card.noun} on this page, so nothing was sent. Ask me again and I'll fill it in from here.`;
|
|
3506
|
+
break;
|
|
3507
|
+
case 'incomplete':
|
|
3508
|
+
this._reviveCard(card, outcome.tool, outcome.pending.values, outcome.missing);
|
|
3509
|
+
card.stage = 'collect';
|
|
3510
|
+
card.message = undefined;
|
|
3511
|
+
break;
|
|
3512
|
+
case 'rejected':
|
|
3513
|
+
case 'failed':
|
|
3514
|
+
this._reviveCard(card, outcome.tool, outcome.pending.values, []);
|
|
3515
|
+
card.stage = 'failed';
|
|
3516
|
+
card.message = outcome.status === 'failed' ? outcome.error : outcome.text || 'That form did not accept the details.';
|
|
3517
|
+
break;
|
|
3518
|
+
case 'submitted': {
|
|
3519
|
+
const note = this._buildDetailItem(outcome.tool, outcome.pending.values, outcome.text).note;
|
|
3520
|
+
card.message = /submitted successfully/.test(note) ? undefined : note;
|
|
3521
|
+
card.stage = 'done';
|
|
3522
|
+
this._active()?.messages.push({
|
|
3523
|
+
role: 'user',
|
|
3524
|
+
content:
|
|
3525
|
+
`[${outcome.tool.name} submitted with: ${JSON.stringify(outcome.pending.values)}]. ` +
|
|
3526
|
+
'Keep these unless I ask to change them.',
|
|
3527
|
+
});
|
|
3528
|
+
break;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
this._paintAction(card);
|
|
3532
|
+
this._saveConversations();
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
2994
3536
|
switch (outcome.status) {
|
|
2995
3537
|
case 'none':
|
|
2996
3538
|
return;
|
|
@@ -3348,6 +3890,9 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
3348
3890
|
if (this._els?.thread) {
|
|
3349
3891
|
this.provider = next ?? this.provider;
|
|
3350
3892
|
this._paintProvenance();
|
|
3893
|
+
// Both the single agent and any configured ones: a page without `agents` kept the old
|
|
3894
|
+
// provider's harness, and answered with the model it had just replaced.
|
|
3895
|
+
this._harness = this._singleHarness();
|
|
3351
3896
|
this._buildAgents();
|
|
3352
3897
|
}
|
|
3353
3898
|
}
|
|
@@ -3551,6 +4096,9 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
3551
4096
|
/* A2UI surfaces inherit the tokens above through --a2ui-* bridges. */
|
|
3552
4097
|
${A2UI_THEME_CSS}
|
|
3553
4098
|
|
|
4099
|
+
/* The action card: forms and handoffs as one message. */
|
|
4100
|
+
${ACTION_CARD_CSS}
|
|
4101
|
+
|
|
3554
4102
|
/* App-icon tile: chevron centered in a 56px tile, Soft Gray, hairline. */
|
|
3555
4103
|
.launcher {
|
|
3556
4104
|
position: fixed; right: 20px; bottom: 20px; z-index: 9998;
|