@runtypelabs/persona 4.19.0 → 4.21.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CG-bLFmk.d.cts → types-B_JdVK9o.d.cts} +2 -0
- package/dist/animations/{types-CG-bLFmk.d.ts → types-B_JdVK9o.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui.d.cts +99 -5
- package/dist/approval-ui.d.ts +99 -5
- package/dist/artifacts-ui.d.cts +99 -5
- package/dist/artifacts-ui.d.ts +99 -5
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +99 -5
- package/dist/context-mentions.d.ts +99 -5
- package/dist/event-stream-view.d.cts +99 -5
- package/dist/event-stream-view.d.ts +99 -5
- package/dist/forms-ui.d.cts +123 -9
- package/dist/forms-ui.d.ts +123 -9
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +51 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -10
- package/dist/index.d.ts +128 -10
- package/dist/index.global.js +34 -34
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +51 -51
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +99 -5
- package/dist/session-reconnect.d.ts +99 -5
- package/dist/smart-dom-reader.d.cts +99 -5
- package/dist/smart-dom-reader.d.ts +99 -5
- package/dist/theme-editor-preview.cjs +53 -53
- package/dist/theme-editor-preview.d.cts +99 -5
- package/dist/theme-editor-preview.d.ts +99 -5
- package/dist/theme-editor-preview.js +31 -31
- package/dist/theme-editor.d.cts +99 -5
- package/dist/theme-editor.d.ts +99 -5
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +46 -5
- package/dist/theme-reference.d.ts +46 -5
- package/dist/theme-reference.js +1 -1
- package/dist/webmcp-runtime.d.cts +16 -0
- package/dist/webmcp-runtime.d.ts +16 -0
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/client-visitor-history.test.ts +342 -3
- package/src/client.test.ts +141 -3
- package/src/client.ts +157 -39
- package/src/components/composer-actions.ts +27 -1
- package/src/components/composer-model-picker.ts +4 -14
- package/src/components/composer-overflow-menu.ts +14 -0
- package/src/components/composer-parts.ts +20 -0
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/index-core.ts +2 -0
- package/src/session.ts +139 -103
- package/src/session.webmcp.test.ts +48 -1
- package/src/styles/widget-styles.test.ts +83 -1
- package/src/styles/widget.css +50 -14
- package/src/theme-reference.ts +1 -1
- package/src/types/theme.ts +47 -5
- package/src/types.ts +63 -0
- package/src/ui.composer-action-visibility.test.ts +202 -0
- package/src/ui.composer-models.test.ts +37 -0
- package/src/ui.composer-modes.test.ts +82 -0
- package/src/ui.composer-overflow.test.ts +78 -0
- package/src/ui.history-lifecycle.test.ts +2 -2
- package/src/ui.ts +77 -6
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1633 -686
- package/src/utils/composer-modes.test.ts +33 -0
- package/src/utils/composer-modes.ts +19 -2
- package/src/utils/tokens-parity.test.ts +20 -0
- package/src/utils/tokens.test.ts +76 -0
- package/src/utils/tokens.ts +4 -4
package/src/client.ts
CHANGED
|
@@ -566,6 +566,54 @@ export class AgentWidgetClient {
|
|
|
566
566
|
return this.clientSession;
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
/** Persona's built-in reconnect transport for client-token sessions. */
|
|
570
|
+
public async reconnectClientTokenStream(ctx: {
|
|
571
|
+
executionId: string;
|
|
572
|
+
after: string;
|
|
573
|
+
signal: AbortSignal;
|
|
574
|
+
}): Promise<Response> {
|
|
575
|
+
let session = await this.initSession();
|
|
576
|
+
let recovered = false;
|
|
577
|
+
|
|
578
|
+
for (;;) {
|
|
579
|
+
const conversationId = session.conversationId;
|
|
580
|
+
const visitorToken = await this.readVisitorToken();
|
|
581
|
+
if (session.durableRecovery?.enabled !== true || !conversationId || !visitorToken) {
|
|
582
|
+
throw new HistoryClientError(
|
|
583
|
+
'visitor_token_missing',
|
|
584
|
+
'Durable recovery is not enabled for this client session'
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const path =
|
|
588
|
+
`${this.clientApiBase()}/v1/client/conversations/` +
|
|
589
|
+
`${encodeURIComponent(conversationId)}/executions/` +
|
|
590
|
+
`${encodeURIComponent(ctx.executionId)}/events`;
|
|
591
|
+
const query = new URLSearchParams({
|
|
592
|
+
sessionId: session.sessionId,
|
|
593
|
+
after: ctx.after,
|
|
594
|
+
});
|
|
595
|
+
const response = await fetch(`${path}?${query}`, {
|
|
596
|
+
method: 'GET',
|
|
597
|
+
headers: {
|
|
598
|
+
'X-Persona-Version': VERSION,
|
|
599
|
+
'X-Visitor-Token': visitorToken,
|
|
600
|
+
},
|
|
601
|
+
signal: ctx.signal,
|
|
602
|
+
});
|
|
603
|
+
if (response.status === 401 && !recovered) {
|
|
604
|
+
recovered = true;
|
|
605
|
+
session = await this.recoverFromUnauthorized(
|
|
606
|
+
await this.readErrorCode(response),
|
|
607
|
+
null,
|
|
608
|
+
true
|
|
609
|
+
);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (!response.ok) throw await this.historyErrorFor(response, true);
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
569
617
|
/**
|
|
570
618
|
* Initialize session for client token mode.
|
|
571
619
|
* Called automatically on first message if not already initialized.
|
|
@@ -709,13 +757,18 @@ export class AgentWidgetClient {
|
|
|
709
757
|
*/
|
|
710
758
|
private async createClientSession(opts: {
|
|
711
759
|
conversationId?: string;
|
|
760
|
+
durableResume?: boolean;
|
|
712
761
|
identityProof?: string | null;
|
|
713
762
|
storedSessionId?: string | null;
|
|
714
763
|
omitVisitorFields?: boolean;
|
|
715
764
|
}): Promise<ClientSession> {
|
|
716
765
|
const historyCapable = this.isHistoryCapable() && !opts.omitVisitorFields;
|
|
766
|
+
// Recovery is negotiated independently from the history UI. New servers
|
|
767
|
+
// return an explicit capability bit; old strict servers reject the
|
|
768
|
+
// additive request field, which the fallback below handles once.
|
|
769
|
+
const durableRecoveryRequested = !opts.omitVisitorFields;
|
|
717
770
|
let visitorToken: string | null = null;
|
|
718
|
-
if (historyCapable) {
|
|
771
|
+
if (historyCapable || durableRecoveryRequested) {
|
|
719
772
|
// Never read persisted state before the controller's stored-state gate.
|
|
720
773
|
await this.historyInternals.historyBootstrapReady;
|
|
721
774
|
visitorToken = await this.readVisitorToken();
|
|
@@ -723,12 +776,16 @@ export class AgentWidgetClient {
|
|
|
723
776
|
|
|
724
777
|
const routed = this.routing();
|
|
725
778
|
const sessionTargetId = routed.agentId ?? routed.flowId;
|
|
726
|
-
const resumeConversationId =
|
|
779
|
+
const resumeConversationId =
|
|
780
|
+
historyCapable || opts.durableResume ? opts.conversationId : undefined;
|
|
727
781
|
const requestBody: Record<string, unknown> = {
|
|
728
782
|
token: this.config.clientToken,
|
|
729
783
|
...(sessionTargetId && { flowId: sessionTargetId }),
|
|
730
784
|
...(historyCapable && { visitorHistory: true }),
|
|
731
|
-
...(
|
|
785
|
+
...(durableRecoveryRequested && { durableRecovery: true }),
|
|
786
|
+
...((historyCapable || durableRecoveryRequested) && visitorToken
|
|
787
|
+
? { visitorToken }
|
|
788
|
+
: {}),
|
|
732
789
|
// Independent of the strict conversationId/sessionId union: a proof also
|
|
733
790
|
// binds the visitor on an ordinary init.
|
|
734
791
|
...(historyCapable && opts.identityProof
|
|
@@ -741,7 +798,7 @@ export class AgentWidgetClient {
|
|
|
741
798
|
: {}),
|
|
742
799
|
};
|
|
743
800
|
|
|
744
|
-
|
|
801
|
+
let response = await fetch(this.getClientApiUrl('init'), {
|
|
745
802
|
method: 'POST',
|
|
746
803
|
headers: {
|
|
747
804
|
'Content-Type': 'application/json',
|
|
@@ -750,6 +807,22 @@ export class AgentWidgetClient {
|
|
|
750
807
|
body: JSON.stringify(requestBody),
|
|
751
808
|
});
|
|
752
809
|
|
|
810
|
+
// Rolling/self-hosted compatibility: pre-recovery servers use a strict
|
|
811
|
+
// init schema and answer 400 to the additive field. Retry once without it;
|
|
812
|
+
// the absent response capability keeps Persona on ordinary streaming.
|
|
813
|
+
if (response.status === 400 && durableRecoveryRequested) {
|
|
814
|
+
const fallbackBody = { ...requestBody };
|
|
815
|
+
delete fallbackBody.durableRecovery;
|
|
816
|
+
response = await fetch(this.getClientApiUrl('init'), {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
headers: {
|
|
819
|
+
'Content-Type': 'application/json',
|
|
820
|
+
'X-Persona-Version': VERSION,
|
|
821
|
+
},
|
|
822
|
+
body: JSON.stringify(fallbackBody),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
753
826
|
if (!response.ok) {
|
|
754
827
|
const error = await response.json().catch(() => ({ error: 'Session initialization failed' }));
|
|
755
828
|
if (historyCapable && response.status === 403 && error.error === 'visitor_history_disabled') {
|
|
@@ -763,7 +836,17 @@ export class AgentWidgetClient {
|
|
|
763
836
|
}
|
|
764
837
|
return this.createClientSession({ ...opts, omitVisitorFields: true });
|
|
765
838
|
}
|
|
766
|
-
if (
|
|
839
|
+
if (response.status === 403 && error.error === 'durable_recovery_disabled') {
|
|
840
|
+
throw new HistoryClientError(
|
|
841
|
+
'history_disabled',
|
|
842
|
+
'Durable recovery is disabled for this surface'
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (
|
|
846
|
+
(historyCapable || opts.durableResume) &&
|
|
847
|
+
response.status === 401 &&
|
|
848
|
+
error.error === 'visitor_required'
|
|
849
|
+
) {
|
|
767
850
|
throw new HistoryClientError('visitor_required', 'Visitor credential no longer resolves');
|
|
768
851
|
}
|
|
769
852
|
if (resumeConversationId && response.status === 404) {
|
|
@@ -797,6 +880,7 @@ export class AgentWidgetClient {
|
|
|
797
880
|
...(data.conversationRevision
|
|
798
881
|
? { conversationRevision: data.conversationRevision }
|
|
799
882
|
: {}),
|
|
883
|
+
...(data.durableRecovery ? { durableRecovery: data.durableRecovery } : {}),
|
|
800
884
|
...(data.visitor ? { visitor: data.visitor } : {}),
|
|
801
885
|
config: {
|
|
802
886
|
welcomeMessage: data.config.welcomeMessage,
|
|
@@ -807,15 +891,13 @@ export class AgentWidgetClient {
|
|
|
807
891
|
}
|
|
808
892
|
|
|
809
893
|
private async _doInitSession(): Promise<ClientSession> {
|
|
810
|
-
if (!this.isHistoryCapable()) {
|
|
811
|
-
const storedSessionId = this.config.getStoredSessionId?.() || null;
|
|
812
|
-
const session = await this.createClientSession({ storedSessionId });
|
|
813
|
-
this.config.setStoredSessionId?.(session.sessionId);
|
|
814
|
-
return session;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
894
|
await this.historyInternals.historyBootstrapReady;
|
|
818
895
|
const previousConversationId = this.config.getStoredConversationId?.() || null;
|
|
896
|
+
const durableResume =
|
|
897
|
+
this.historyInternals.shouldResumeDurableConversation?.() === true;
|
|
898
|
+
if (!this.isHistoryCapable() && !durableResume) {
|
|
899
|
+
return this.ordinaryInit(previousConversationId, false);
|
|
900
|
+
}
|
|
819
901
|
const storedToken = await this.readVisitorToken();
|
|
820
902
|
|
|
821
903
|
// Boot resume: reopening the record beats replaying a possibly idle-expired
|
|
@@ -824,15 +906,23 @@ export class AgentWidgetClient {
|
|
|
824
906
|
try {
|
|
825
907
|
const resumed = await this.createClientSession({
|
|
826
908
|
conversationId: previousConversationId,
|
|
909
|
+
durableResume,
|
|
827
910
|
});
|
|
911
|
+
if (durableResume && resumed.durableRecovery?.enabled !== true) {
|
|
912
|
+
this.historyInternals.setStoredResumableHandle?.(null);
|
|
913
|
+
}
|
|
828
914
|
return this.finishInit(resumed, previousConversationId, false);
|
|
829
915
|
} catch (error) {
|
|
830
916
|
if (
|
|
831
917
|
!isHistoryClientError(error, 'not_found') &&
|
|
832
|
-
!isHistoryClientError(error, 'visitor_required')
|
|
918
|
+
!isHistoryClientError(error, 'visitor_required') &&
|
|
919
|
+
!isHistoryClientError(error, 'history_disabled')
|
|
833
920
|
) {
|
|
834
921
|
throw error;
|
|
835
922
|
}
|
|
923
|
+
if (durableResume) {
|
|
924
|
+
this.historyInternals.setStoredResumableHandle?.(null);
|
|
925
|
+
}
|
|
836
926
|
// Record gone or credential dead: exactly one ordinary fallback, never a loop.
|
|
837
927
|
return this.ordinaryInit(previousConversationId, true);
|
|
838
928
|
}
|
|
@@ -851,7 +941,7 @@ export class AgentWidgetClient {
|
|
|
851
941
|
continuityBroken: boolean
|
|
852
942
|
): Promise<ClientSession> {
|
|
853
943
|
const store = this.historyInternals.visitorStore;
|
|
854
|
-
if (store &&
|
|
944
|
+
if (store && !(await this.readVisitorToken())) {
|
|
855
945
|
return store.withFirstInitLock(() =>
|
|
856
946
|
this.doOrdinaryInit(previousConversationId, continuityBroken)
|
|
857
947
|
);
|
|
@@ -893,7 +983,11 @@ export class AgentWidgetClient {
|
|
|
893
983
|
* unowned; one immediate re-init with `{visitorToken, sessionId}` claims it.
|
|
894
984
|
*/
|
|
895
985
|
private async claimFirstConversation(first: ClientSession): Promise<ClientSession> {
|
|
896
|
-
if (
|
|
986
|
+
if (
|
|
987
|
+
!first.visitor?.token ||
|
|
988
|
+
this.claimInFlight ||
|
|
989
|
+
(!this.isHistoryCapable() && first.durableRecovery?.enabled !== true)
|
|
990
|
+
) {
|
|
897
991
|
return first;
|
|
898
992
|
}
|
|
899
993
|
this.claimInFlight = true;
|
|
@@ -1754,6 +1848,10 @@ export class AgentWidgetClient {
|
|
|
1754
1848
|
|
|
1755
1849
|
// Build the standard payload to get context/metadata from middleware
|
|
1756
1850
|
const basePayload = await this.buildPayload(options.messages);
|
|
1851
|
+
const recoveryVisitorToken =
|
|
1852
|
+
session.durableRecovery?.enabled === true
|
|
1853
|
+
? await this.readVisitorToken()
|
|
1854
|
+
: null;
|
|
1757
1855
|
|
|
1758
1856
|
// Build the chat request payload with message IDs for feedback tracking
|
|
1759
1857
|
// Filter out sessionId from metadata if present (it's only for local storage)
|
|
@@ -1813,6 +1911,9 @@ export class AgentWidgetClient {
|
|
|
1813
1911
|
headers: {
|
|
1814
1912
|
'Content-Type': 'application/json',
|
|
1815
1913
|
'X-Persona-Version': VERSION,
|
|
1914
|
+
...(recoveryVisitorToken
|
|
1915
|
+
? { 'X-Visitor-Token': recoveryVisitorToken }
|
|
1916
|
+
: {}),
|
|
1816
1917
|
},
|
|
1817
1918
|
body: JSON.stringify(chatRequest),
|
|
1818
1919
|
signal: options.signal,
|
|
@@ -2221,7 +2322,7 @@ export class AgentWidgetClient {
|
|
|
2221
2322
|
public async resumeFlow(
|
|
2222
2323
|
executionId: string,
|
|
2223
2324
|
toolOutputs: Record<string, unknown>,
|
|
2224
|
-
options?: { streamResponse?: boolean; signal?: AbortSignal }
|
|
2325
|
+
options?: { streamResponse?: boolean; signal?: AbortSignal; after?: string }
|
|
2225
2326
|
): Promise<Response> {
|
|
2226
2327
|
const isClientToken = this.isClientTokenMode();
|
|
2227
2328
|
const url = isClientToken
|
|
@@ -2236,12 +2337,18 @@ export class AgentWidgetClient {
|
|
|
2236
2337
|
// trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
|
|
2237
2338
|
// PR #214 r3367875360.)
|
|
2238
2339
|
let resumeSessionId: string | undefined;
|
|
2340
|
+
let resumeVisitorToken: string | null = null;
|
|
2239
2341
|
if (isClientToken) {
|
|
2240
|
-
|
|
2342
|
+
const session = await this.initSession();
|
|
2343
|
+
resumeSessionId = session.sessionId;
|
|
2344
|
+
if (session.durableRecovery?.enabled === true) {
|
|
2345
|
+
resumeVisitorToken = await this.readVisitorToken();
|
|
2346
|
+
}
|
|
2241
2347
|
}
|
|
2242
2348
|
|
|
2243
2349
|
let headers: Record<string, string> = {
|
|
2244
2350
|
'Content-Type': 'application/json',
|
|
2351
|
+
...(resumeVisitorToken ? { 'X-Visitor-Token': resumeVisitorToken } : {}),
|
|
2245
2352
|
...this.headers
|
|
2246
2353
|
};
|
|
2247
2354
|
if (this.getHeaders) {
|
|
@@ -2252,6 +2359,7 @@ export class AgentWidgetClient {
|
|
|
2252
2359
|
executionId,
|
|
2253
2360
|
toolOutputs,
|
|
2254
2361
|
streamResponse: options?.streamResponse ?? true,
|
|
2362
|
+
...(options?.after ? { after: options.after } : {}),
|
|
2255
2363
|
};
|
|
2256
2364
|
// Thread the (refreshed) sessionId through like `/v1/client/chat` does.
|
|
2257
2365
|
if (resumeSessionId) {
|
|
@@ -2737,9 +2845,10 @@ export class AgentWidgetClient {
|
|
|
2737
2845
|
// bubble-id segmentation on the wire in place of the legacy `partId`:
|
|
2738
2846
|
// a new block id means a new bubble, sealed at `text_complete`/tool boundaries.
|
|
2739
2847
|
let currentTextBlockId: string | null = null;
|
|
2740
|
-
// Raw text accumulated for the open
|
|
2741
|
-
//
|
|
2742
|
-
|
|
2848
|
+
// Raw text accumulated for the open text block, on both the flow and agent
|
|
2849
|
+
// paths — lets a whitespace-only flow block resolve without a stray bubble,
|
|
2850
|
+
// and gives the structured-content parser the whole block on either path.
|
|
2851
|
+
let pendingTextRaw = "";
|
|
2743
2852
|
// Nested flow-as-tool attribution (PR #4602): a text/reasoning block whose
|
|
2744
2853
|
// `parentToolCallId` matches a `tool_start.toolCallId` belongs to a flow
|
|
2745
2854
|
// running as that tool. Keyed by the wire block id, these route the block's
|
|
@@ -3134,20 +3243,20 @@ export class AgentWidgetClient {
|
|
|
3134
3243
|
finalizeCleanup();
|
|
3135
3244
|
};
|
|
3136
3245
|
|
|
3137
|
-
// === Unified
|
|
3138
|
-
//
|
|
3139
|
-
// `text_start`/`text_complete`) and can be structured JSON, so each block
|
|
3140
|
-
//
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
3143
|
-
//
|
|
3144
|
-
//
|
|
3246
|
+
// === Unified text channel ===
|
|
3247
|
+
// Prompt-step and agent text both stream as `text_delta` blocks (segmented by
|
|
3248
|
+
// `text_start`/`text_complete`) and can be structured JSON, so each block runs
|
|
3249
|
+
// through the per-bubble structured-content parser. This is the legacy
|
|
3250
|
+
// step_delta parser core, re-keyed from `partId` to the wire block-id bubble.
|
|
3251
|
+
// The caller materializes the bubble lazily (whitespace-only blocks around
|
|
3252
|
+
// tool boundaries never leave a stray bubble) and `step_complete.result.response`
|
|
3253
|
+
// reconciles the authoritative final.
|
|
3145
3254
|
let lastSealedFlowBubble: AgentWidgetMessage | null = null;
|
|
3146
3255
|
|
|
3147
|
-
// Stream one accumulated chunk of
|
|
3256
|
+
// Stream one accumulated chunk of block text through the parser, setting
|
|
3148
3257
|
// display `content` (extracted) + `rawContent` (raw) and emitting. Mirrors the
|
|
3149
3258
|
// legacy step_delta chunk path; plain text bypasses the structured parser.
|
|
3150
|
-
const
|
|
3259
|
+
const applyTextChunk = (
|
|
3151
3260
|
assistant: AgentWidgetMessage,
|
|
3152
3261
|
accumulatedRaw: string,
|
|
3153
3262
|
chunk: string,
|
|
@@ -3773,7 +3882,7 @@ export class AgentWidgetClient {
|
|
|
3773
3882
|
}
|
|
3774
3883
|
currentTextBlockId =
|
|
3775
3884
|
typeof payload.id === "string" ? payload.id : currentTextBlockId;
|
|
3776
|
-
|
|
3885
|
+
pendingTextRaw = "";
|
|
3777
3886
|
} else if (payloadType === "text_delta") {
|
|
3778
3887
|
// Nested flow-as-tool text: route to the parent tool's row, through the
|
|
3779
3888
|
// same structured-content parser, never the top-level assistant channel.
|
|
@@ -3793,7 +3902,7 @@ export class AgentWidgetClient {
|
|
|
3793
3902
|
executionId: payload.executionId,
|
|
3794
3903
|
parentToolId: nestedParent,
|
|
3795
3904
|
};
|
|
3796
|
-
|
|
3905
|
+
applyTextChunk(nested, nestedRaw, nestedDelta, undefined);
|
|
3797
3906
|
continue;
|
|
3798
3907
|
}
|
|
3799
3908
|
currentTextBlockId =
|
|
@@ -3804,27 +3913,36 @@ export class AgentWidgetClient {
|
|
|
3804
3913
|
// block-id bubble. Materialize lazily so a whitespace-only block
|
|
3805
3914
|
// (newlines around a tool boundary) never leaves a stray bubble.
|
|
3806
3915
|
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
3807
|
-
|
|
3808
|
-
if (
|
|
3916
|
+
pendingTextRaw += delta;
|
|
3917
|
+
if (pendingTextRaw.trim() === "") continue;
|
|
3809
3918
|
const assistant = ensureAssistantMessage();
|
|
3810
3919
|
assistant.agentMetadata = {
|
|
3811
3920
|
executionId: payload.executionId,
|
|
3812
3921
|
iteration: payload.iteration,
|
|
3813
3922
|
};
|
|
3814
|
-
|
|
3923
|
+
applyTextChunk(assistant, pendingTextRaw, delta, undefined);
|
|
3815
3924
|
lastAssistantInTurn = assistant;
|
|
3816
3925
|
continue;
|
|
3817
3926
|
}
|
|
3927
|
+
// Agent text can be structured JSON too, whenever the host configures a
|
|
3928
|
+
// custom `streamParser` (e.g. the page-context demo's `{"text": ...}` /
|
|
3929
|
+
// `{"action": "add_to_cart", ...}` envelope). Accumulate the open block's
|
|
3930
|
+
// raw text and run it through the same structured-content path the flow
|
|
3931
|
+
// branch uses, so the parser sees the whole block and `rawContent` stays
|
|
3932
|
+
// populated for the action manager. With the default plain-text parser
|
|
3933
|
+
// this is byte-identical to appending the delta, so agent demos that
|
|
3934
|
+
// stream prose are unaffected.
|
|
3935
|
+
const agentDelta = typeof payload.delta === "string" ? payload.delta : "";
|
|
3936
|
+
pendingTextRaw += agentDelta;
|
|
3818
3937
|
const assistant = ensureAssistantMessage();
|
|
3819
|
-
assistant.content += payload.delta ?? '';
|
|
3820
3938
|
assistant.agentMetadata = {
|
|
3821
3939
|
executionId: payload.executionId,
|
|
3822
3940
|
iteration: payload.iteration,
|
|
3823
3941
|
turnId: openTurnId ?? undefined,
|
|
3824
3942
|
agentName: agentExecution?.agentName
|
|
3825
3943
|
};
|
|
3944
|
+
applyTextChunk(assistant, pendingTextRaw, agentDelta, undefined);
|
|
3826
3945
|
lastAssistantInTurn = assistant;
|
|
3827
|
-
emitMessage(assistant);
|
|
3828
3946
|
} else if (payloadType === "text_complete") {
|
|
3829
3947
|
// Nested flow-as-tool text block close: seal its parent-tool-row message.
|
|
3830
3948
|
const completeBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -3857,7 +3975,7 @@ export class AgentWidgetClient {
|
|
|
3857
3975
|
assistantMessage = null;
|
|
3858
3976
|
}
|
|
3859
3977
|
currentTextBlockId = null;
|
|
3860
|
-
|
|
3978
|
+
pendingTextRaw = "";
|
|
3861
3979
|
} else if (payloadType === "step_complete") {
|
|
3862
3980
|
// Only process completions for prompt steps, not tool/context steps
|
|
3863
3981
|
const stepType = (payload as any).stepType;
|
|
@@ -4201,7 +4319,7 @@ export class AgentWidgetClient {
|
|
|
4201
4319
|
assistantMessage = null;
|
|
4202
4320
|
}
|
|
4203
4321
|
currentTextBlockId = null;
|
|
4204
|
-
|
|
4322
|
+
pendingTextRaw = "";
|
|
4205
4323
|
lastSealedFlowBubble = null;
|
|
4206
4324
|
|
|
4207
4325
|
// `terminal: true` marks this as a graceful finish (not a drop). The
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
ComposerActionOverflowConfig,
|
|
33
33
|
ComposerActionPlacement,
|
|
34
34
|
ComposerActionPresentation,
|
|
35
|
+
ComposerActionVisibility,
|
|
35
36
|
ComposerButtonAction,
|
|
36
37
|
ComposerCustomAction,
|
|
37
38
|
ComposerState,
|
|
@@ -345,6 +346,28 @@ const evaluate = (
|
|
|
345
346
|
return typeof value === "function" ? value(state) === true : value === true;
|
|
346
347
|
};
|
|
347
348
|
|
|
349
|
+
/**
|
|
350
|
+
* The draft state `sendButton.visibility` reads: text, a pending attachment, or
|
|
351
|
+
* a live stream all count as "there is something in flight".
|
|
352
|
+
*/
|
|
353
|
+
const hasDraftContent = (state: Readonly<ComposerState>): boolean =>
|
|
354
|
+
state.phase === "streaming" ||
|
|
355
|
+
state.text.trim().length > 0 ||
|
|
356
|
+
state.attachments.length > 0;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* `ComposerAction.visibility` against the current draft. Evaluated on every
|
|
360
|
+
* state-driven pass, so the answer is always render-time, never patched on.
|
|
361
|
+
*/
|
|
362
|
+
export function matchesComposerActionVisibility(
|
|
363
|
+
visibility: ComposerActionVisibility | undefined,
|
|
364
|
+
state: Readonly<ComposerState>
|
|
365
|
+
): boolean {
|
|
366
|
+
if (!visibility || visibility === "always") return true;
|
|
367
|
+
const drafting = hasDraftContent(state);
|
|
368
|
+
return visibility === "when-text" ? drafting : !drafting;
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
const isCustom = (action: ComposerAction): action is ComposerCustomAction =>
|
|
349
372
|
action.kind === "custom";
|
|
350
373
|
|
|
@@ -937,7 +960,10 @@ export function createComposerActionRenderer(
|
|
|
937
960
|
let entry = entries.get(resolved.id);
|
|
938
961
|
if (!entry) continue;
|
|
939
962
|
const action = resolved.action;
|
|
940
|
-
const visible = action
|
|
963
|
+
const visible = action
|
|
964
|
+
? evaluate(action.visible, state, true) &&
|
|
965
|
+
matchesComposerActionVisibility(action.visibility, state)
|
|
966
|
+
: true;
|
|
941
967
|
if (entry.visible !== visible) {
|
|
942
968
|
entry.visible = visible;
|
|
943
969
|
// Managed elements leave the DOM when hidden; the node (and any custom
|
|
@@ -18,6 +18,7 @@ import { createPopover, type PopoverHandle } from "../plugin-kit";
|
|
|
18
18
|
import type { ComposerCustomAction, ComposerModel } from "../types";
|
|
19
19
|
import { createElement, createNode } from "../utils/dom";
|
|
20
20
|
import { renderLucideIcon } from "../utils/icons";
|
|
21
|
+
import { forwardMenuTokens } from "./composer-parts";
|
|
21
22
|
|
|
22
23
|
export const COMPOSER_MODEL_PICKER_ACTION_ID = "core:model";
|
|
23
24
|
export const COMPOSER_MODEL_PICKER_ORDER = 700;
|
|
@@ -25,9 +26,8 @@ export const COMPOSER_MODEL_PICKER_ORDER = 700;
|
|
|
25
26
|
const ROW_ICON_SIZE = 16;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Tokens the popover panel reads
|
|
29
|
-
*
|
|
30
|
-
* the stylesheet's fallbacks stand.
|
|
29
|
+
* Tokens the popover panel reads, forwarded onto the portaled panel on every
|
|
30
|
+
* open; unset ones clear so the stylesheet's fallbacks stand.
|
|
31
31
|
*/
|
|
32
32
|
const MENU_TOKEN_VARS = [
|
|
33
33
|
"--persona-components-composer-modelPicker-menuBackground",
|
|
@@ -62,16 +62,6 @@ export interface ComposerModelPicker {
|
|
|
62
62
|
repaint: () => void;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const forwardMenuTokens = (from: HTMLElement, to: HTMLElement): void => {
|
|
66
|
-
const view = from.ownerDocument?.defaultView;
|
|
67
|
-
if (!view) return;
|
|
68
|
-
const computed = view.getComputedStyle(from);
|
|
69
|
-
for (const name of MENU_TOKEN_VARS) {
|
|
70
|
-
const value = computed.getPropertyValue(name).trim();
|
|
71
|
-
if (value) to.style.setProperty(name, value);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
65
|
/** The wrapper both presentations share: one grid cell, control plus chevron. */
|
|
76
66
|
const createWrapper = (): { wrapper: HTMLElement; chevron: HTMLElement } => {
|
|
77
67
|
// `appearance: none` drops the platform chevron, which Chrome draws
|
|
@@ -221,7 +211,7 @@ export function createComposerModelPickerAction(
|
|
|
221
211
|
onDismiss: () => trigger.setAttribute("aria-expanded", "false"),
|
|
222
212
|
});
|
|
223
213
|
}
|
|
224
|
-
forwardMenuTokens(trigger, menu);
|
|
214
|
+
forwardMenuTokens(trigger, menu, MENU_TOKEN_VARS);
|
|
225
215
|
popover.open();
|
|
226
216
|
trigger.setAttribute("aria-expanded", "true");
|
|
227
217
|
const ids = [...rows.keys()];
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
COMPOSER_CONTROL_CLASS,
|
|
20
20
|
COMPOSER_CONTROL_GLYPH_CLASS,
|
|
21
21
|
COMPOSER_CONTROL_ICON_FALLBACK_PX,
|
|
22
|
+
forwardMenuTokens,
|
|
22
23
|
} from "./composer-parts";
|
|
23
24
|
|
|
24
25
|
export interface ComposerOverflowMenuOptions {
|
|
@@ -54,6 +55,18 @@ export interface ComposerOverflowMenu {
|
|
|
54
55
|
const focusableSelector =
|
|
55
56
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Panel surface tokens, forwarded onto the portaled panel on every open; unset
|
|
60
|
+
* ones clear so the stylesheet's fallbacks stand.
|
|
61
|
+
*/
|
|
62
|
+
const MENU_TOKEN_VARS = [
|
|
63
|
+
"--persona-components-composer-overflowMenu-background",
|
|
64
|
+
"--persona-components-composer-overflowMenu-borderColor",
|
|
65
|
+
"--persona-components-composer-overflowMenu-borderRadius",
|
|
66
|
+
"--persona-components-composer-overflowMenu-foreground",
|
|
67
|
+
"--persona-components-composer-overflowMenu-shadow",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Set on the menu row that holds keyboard focus. Rows that shade on
|
|
59
72
|
* `:focus-visible` do not need it; wrapper rows around a live control do.
|
|
@@ -249,6 +262,7 @@ export function createComposerOverflowMenu(
|
|
|
249
262
|
const open = (focusIndex = 0, viaKeyboard = false): void => {
|
|
250
263
|
if (destroyed || isOpen()) return;
|
|
251
264
|
keyboardModality = viaKeyboard;
|
|
265
|
+
forwardMenuTokens(triggerButton, panel, MENU_TOKEN_VARS);
|
|
252
266
|
ensurePopover().open();
|
|
253
267
|
triggerButton.setAttribute("aria-expanded", "true");
|
|
254
268
|
const targets = focusables();
|
|
@@ -41,6 +41,26 @@ export const COMPOSER_CONTROL_FALLBACK_PX = 40;
|
|
|
41
41
|
/** Icon-size token default in CSS pixels, for the same JS-derived cases. */
|
|
42
42
|
export const COMPOSER_CONTROL_ICON_FALLBACK_PX = 24;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Forward theme vars onto a portaled menu panel, which lives outside the
|
|
46
|
+
* themed mount. The panel is reused across opens, so an unset var must clear
|
|
47
|
+
* any previously stamped value for the stylesheet fallback to paint again.
|
|
48
|
+
*/
|
|
49
|
+
export const forwardMenuTokens = (
|
|
50
|
+
from: HTMLElement,
|
|
51
|
+
to: HTMLElement,
|
|
52
|
+
names: readonly string[]
|
|
53
|
+
): void => {
|
|
54
|
+
const view = from.ownerDocument?.defaultView;
|
|
55
|
+
if (!view) return;
|
|
56
|
+
const computed = view.getComputedStyle(from);
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const value = computed.getPropertyValue(name).trim();
|
|
59
|
+
if (value) to.style.setProperty(name, value);
|
|
60
|
+
else to.style.removeProperty(name);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
export interface ComposerTextareaParts {
|
|
45
65
|
textarea: HTMLTextAreaElement;
|
|
46
66
|
/**
|
package/src/index-core.ts
CHANGED
|
@@ -91,6 +91,7 @@ export type {
|
|
|
91
91
|
ComposerActionBase,
|
|
92
92
|
ComposerActionPlacement,
|
|
93
93
|
ComposerActionPresentation,
|
|
94
|
+
ComposerActionVisibility,
|
|
94
95
|
ComposerButtonAction,
|
|
95
96
|
ComposerCustomAction,
|
|
96
97
|
ComposerActionContext,
|
|
@@ -103,6 +104,7 @@ export type {
|
|
|
103
104
|
ComposerModelPickerPresentation,
|
|
104
105
|
// Composer modes + mode groups (composer.modes, composer.modeGroups)
|
|
105
106
|
ComposerMode,
|
|
107
|
+
ComposerModeChipVisibility,
|
|
106
108
|
ComposerModeGroup,
|
|
107
109
|
ComposerModeGroupPresentation,
|
|
108
110
|
// Composer baseline configuration (submit keys, sizing, locks)
|