@genesislcap/ai-assistant 14.479.0 → 14.480.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/ai-assistant.api.json +546 -2
- package/dist/ai-assistant.d.ts +73 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +32 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +7 -0
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/flowing-waves-indicator.d.ts +1 -1
- package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -1
- package/dist/dts/components/halo-overlay.d.ts +1 -0
- package/dist/dts/components/halo-overlay.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +4 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/components/waves-indicator.d.ts +1 -1
- package/dist/dts/components/waves-indicator.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +29 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +2 -0
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/message-partition.d.ts +1 -0
- package/dist/dts/utils/message-partition.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +88 -8
- package/dist/esm/components/chat-driver/chat-driver.test.js +104 -0
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +48 -6
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +62 -0
- package/dist/esm/components/flowing-waves-indicator.js +22 -11
- package/dist/esm/components/halo-overlay.js +30 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +14 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +7 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +18 -8
- package/dist/esm/components/waves-indicator.js +22 -8
- package/dist/esm/main/main.js +73 -6
- package/dist/esm/main/main.styles.js +6 -0
- package/dist/esm/main/main.template.js +21 -2
- package/dist/esm/state/ai-assistant-slice.js +4 -0
- package/dist/esm/utils/message-partition.js +10 -1
- package/dist/esm/utils/message-partition.test.js +2 -0
- package/package.json +17 -17
- package/src/components/ai-driver/ai-driver.ts +17 -0
- package/src/components/chat-driver/chat-driver.test.ts +143 -0
- package/src/components/chat-driver/chat-driver.ts +105 -4
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +81 -0
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +51 -5
- package/src/components/flowing-waves-indicator.ts +26 -6
- package/src/components/halo-overlay.ts +31 -6
- package/src/components/orchestrating-driver/orchestrating-driver.ts +18 -0
- package/src/components/settings-modal/settings-modal.styles.ts +7 -0
- package/src/components/settings-modal/settings-modal.template.ts +36 -16
- package/src/components/waves-indicator.ts +25 -5
- package/src/main/main.styles.ts +6 -0
- package/src/main/main.template.ts +25 -2
- package/src/main/main.ts +65 -1
- package/src/state/ai-assistant-slice.ts +5 -0
- package/src/utils/message-partition.test.ts +2 -0
- package/src/utils/message-partition.ts +11 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
1
|
+
import { __awaiter, __rest } from "tslib";
|
|
2
2
|
import { isObservableAIProviderRegistry, MalformedFunctionCallError, ResponseTruncatedError, } from '@genesislcap/foundation-ai';
|
|
3
3
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
4
4
|
import { resolveChatProvider } from '../../config/validate-providers';
|
|
@@ -76,6 +76,13 @@ export class ChatDriver extends EventTarget {
|
|
|
76
76
|
/** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
|
|
77
77
|
this.turnStartedAt = 0;
|
|
78
78
|
this.pendingInteractions = new Map();
|
|
79
|
+
// GENC-1410 (Error 3): when several assistant views share this one driver (e.g. a bubble and a
|
|
80
|
+
// docked panel), each interaction's widget must be rendered live — and thus fire any side
|
|
81
|
+
// effect — on exactly ONE view. These fields elect that owner and de-dupe execution.
|
|
82
|
+
/** Assistant host ids currently wired to this driver, in registration (insertion) order. */
|
|
83
|
+
this.hosts = new Set();
|
|
84
|
+
/** Interaction id → host id that claimed its side effect (first-writer-wins latch). */
|
|
85
|
+
this.startedSideEffects = new Map();
|
|
79
86
|
/**
|
|
80
87
|
* Tool-declared condensation policies, keyed by tool-call id. Populated by
|
|
81
88
|
* `condenseWhen` (first-wins per call); read by `applyCondensation` before each
|
|
@@ -761,6 +768,53 @@ export class ChatDriver extends EventTarget {
|
|
|
761
768
|
isBusy() {
|
|
762
769
|
return this.busy;
|
|
763
770
|
}
|
|
771
|
+
// --- GENC-1410 (Error 3): single-instance execution of side-effecting widgets --------------
|
|
772
|
+
/** Register an assistant view (host) wired to this shared driver. @internal */
|
|
773
|
+
registerHost(hostId) {
|
|
774
|
+
this.hosts.add(hostId);
|
|
775
|
+
}
|
|
776
|
+
/** Deregister a host on unwire/disconnect. @internal */
|
|
777
|
+
unregisterHost(hostId) {
|
|
778
|
+
this.hosts.delete(hostId);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Record the host whose subtree the user most recently interacted in — the primary signal for
|
|
782
|
+
* electing which single view renders an interaction's widget. Sticky (unlike focus): it only
|
|
783
|
+
* moves when the user actually uses a different assistant view, not when they click elsewhere
|
|
784
|
+
* in the layout. @internal
|
|
785
|
+
*/
|
|
786
|
+
setActiveHost(hostId) {
|
|
787
|
+
this.lastActiveHostId = hostId;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* First-writer-wins latch guarding a side effect. A host claims before rendering the live
|
|
791
|
+
* widget: the first host to claim an interaction id gets `true` (run it); a *different* host
|
|
792
|
+
* gets `false` (render passive, do NOT re-fire). Re-claim by the SAME host returns `true`, so a
|
|
793
|
+
* benign re-render of the owner keeps its widget. Single-threaded, so check-and-set is race-free
|
|
794
|
+
* — the backstop that makes duplicate execution impossible even if owner election ever slips. @internal
|
|
795
|
+
*/
|
|
796
|
+
claimSideEffect(interactionId, hostId) {
|
|
797
|
+
const existing = this.startedSideEffects.get(interactionId);
|
|
798
|
+
if (existing === undefined) {
|
|
799
|
+
this.startedSideEffects.set(interactionId, hostId);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return existing === hostId;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Elect the single host that owns (renders + runs) an interaction's widget: the most-recently-
|
|
806
|
+
* active host if still connected, else a deterministic default (first registered host — `Set`
|
|
807
|
+
* preserves insertion order) so exactly one owner is always chosen. Returns `undefined` only when
|
|
808
|
+
* no host is registered, in which case the interaction renders everywhere (fail-open) not nowhere.
|
|
809
|
+
*/
|
|
810
|
+
electInteractionOwner() {
|
|
811
|
+
if (this.lastActiveHostId && this.hosts.has(this.lastActiveHostId)) {
|
|
812
|
+
return this.lastActiveHostId;
|
|
813
|
+
}
|
|
814
|
+
// First registered host (`Set` preserves insertion order); `undefined` when none.
|
|
815
|
+
const [firstHost] = this.hosts;
|
|
816
|
+
return firstHost;
|
|
817
|
+
}
|
|
764
818
|
/**
|
|
765
819
|
* Wire a parent driver as the host for this driver's interactions. When set,
|
|
766
820
|
* `requestInteraction` delegates upward so the widget renders in (and
|
|
@@ -803,6 +857,10 @@ export class ChatDriver extends EventTarget {
|
|
|
803
857
|
const chatInputDuringExecution = options === null || options === void 0 ? void 0 : options.chatInputDuringExecution;
|
|
804
858
|
const timeoutMs = options === null || options === void 0 ? void 0 : options.timeoutMs;
|
|
805
859
|
const presentation = options === null || options === void 0 ? void 0 : options.presentation;
|
|
860
|
+
// GENC-1410 (Error 3): a widget is rendered live by exactly one host. Elect the owner now
|
|
861
|
+
// (most-recently-active host, deterministic fallback) and stamp it on the interaction so every
|
|
862
|
+
// host can compare its own id and render passively when it differs — one server call, not N.
|
|
863
|
+
const ownerHostId = this.electInteractionOwner();
|
|
806
864
|
return new Promise((resolve, reject) => {
|
|
807
865
|
this.pendingInteractions.set(interactionId, {
|
|
808
866
|
resolve,
|
|
@@ -839,9 +897,9 @@ export class ChatDriver extends EventTarget {
|
|
|
839
897
|
this.appendToHistory({
|
|
840
898
|
role: 'assistant',
|
|
841
899
|
content: '',
|
|
842
|
-
interaction: Object.assign({ interactionId,
|
|
900
|
+
interaction: Object.assign(Object.assign({ interactionId,
|
|
843
901
|
componentName,
|
|
844
|
-
data }, (presentation ? { presentation } : {})),
|
|
902
|
+
data }, (presentation ? { presentation } : {})), (ownerHostId ? { ownerHostId } : {})),
|
|
845
903
|
});
|
|
846
904
|
});
|
|
847
905
|
});
|
|
@@ -895,6 +953,8 @@ export class ChatDriver extends EventTarget {
|
|
|
895
953
|
agenticActivityBus.publish('interaction-resolved', undefined);
|
|
896
954
|
interaction.resolve(result);
|
|
897
955
|
this.pendingInteractions.delete(interactionId);
|
|
956
|
+
// GENC-1410: release the side-effect claim so a re-request with a fresh id starts clean.
|
|
957
|
+
this.startedSideEffects.delete(interactionId);
|
|
898
958
|
}
|
|
899
959
|
else {
|
|
900
960
|
logger.warn(`Interaction with ID ${interactionId} not found.`);
|
|
@@ -1764,12 +1824,32 @@ export class ChatDriver extends EventTarget {
|
|
|
1764
1824
|
}
|
|
1765
1825
|
return { reason: 'done' };
|
|
1766
1826
|
}
|
|
1767
|
-
else if (isThinkingStep) {
|
|
1768
|
-
this.appendToHistory(Object.assign(Object.assign({}, response), { toolCalls: undefined, thinking: true }));
|
|
1769
|
-
this.appendToHistory(Object.assign(Object.assign({}, response), { content: '' }));
|
|
1770
|
-
}
|
|
1771
1827
|
else {
|
|
1772
|
-
|
|
1828
|
+
// Split one model response into separate, individually-toggleable messages so each has its
|
|
1829
|
+
// own visibility toggle and debug-log category:
|
|
1830
|
+
// - reasoning (chain-of-thought summary) → category 'reasoning', hidden unless showThinkingSteps
|
|
1831
|
+
// - narration (interstitial prose emitted alongside a tool call) → category 'narration',
|
|
1832
|
+
// hidden unless showNarration
|
|
1833
|
+
// - the answer / tool-call message → always shown; carries this turn's usage.
|
|
1834
|
+
// Cost invariant (GENC-1410): exactly ONE message carries cost/tokens and is appended LAST, so
|
|
1835
|
+
// `sumCosts`/`sumTokens` don't double-count and `contextTokens` reads it. Reasoning/narration are
|
|
1836
|
+
// display-only (usage undefined) and are skipped when building the provider request. `model` /
|
|
1837
|
+
// `provider` / `providerName` stay on every split message so each is still attributed.
|
|
1838
|
+
const { reasoning } = response, rest = __rest(response, ["reasoning"]);
|
|
1839
|
+
const displayOnly = { cost: undefined, inputTokens: undefined, outputTokens: undefined };
|
|
1840
|
+
if (reasoning) {
|
|
1841
|
+
this.appendToHistory(Object.assign(Object.assign(Object.assign({}, rest), displayOnly), { content: reasoning, toolCalls: undefined, category: 'reasoning' }));
|
|
1842
|
+
}
|
|
1843
|
+
if (isThinkingStep) {
|
|
1844
|
+
// content + tool call → the content is interstitial narration, not the final answer.
|
|
1845
|
+
this.appendToHistory(Object.assign(Object.assign(Object.assign({}, rest), displayOnly), { toolCalls: undefined, category: 'narration' }));
|
|
1846
|
+
this.appendToHistory(Object.assign(Object.assign({}, rest), { content: '' }));
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
// No tool call → `content` is the final answer (always shown), or this is a bare tool-call
|
|
1850
|
+
// turn with no narration. Either way it carries this turn's usage; append it last.
|
|
1851
|
+
this.appendToHistory(rest);
|
|
1852
|
+
}
|
|
1773
1853
|
}
|
|
1774
1854
|
// Reset retry budgets on any productive (non-empty) response, so the caps mean
|
|
1775
1855
|
// "N CONSECUTIVE failures" not "N total per turn".
|
|
@@ -3,6 +3,8 @@ import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
|
3
3
|
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
4
4
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
5
5
|
import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
|
|
6
|
+
import { sumCosts } from '../../utils/sum-costs';
|
|
7
|
+
import { sumTokens } from '../../utils/sum-tokens';
|
|
6
8
|
import { createToolFold } from '../../utils/tool-fold';
|
|
7
9
|
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
8
10
|
// jsdom's EventTarget rather than Node's native one (see the file). None of the
|
|
@@ -773,6 +775,63 @@ subagent('times out (without hanging) when the sub-agent is parked on a user int
|
|
|
773
775
|
}));
|
|
774
776
|
subagent.run();
|
|
775
777
|
// ---------------------------------------------------------------------------
|
|
778
|
+
// cost / token attribution on the thinking-step split (GENC-1410)
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
const costAttribution = createLogicSuite('ChatDriver cost attribution (thinking-step split)');
|
|
781
|
+
costAttribution('a text-plus-tool-call response contributes its cost and tokens exactly once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
782
|
+
const config = agent({
|
|
783
|
+
name: 'Worker',
|
|
784
|
+
toolDefinitions: () => [def('do_thing')],
|
|
785
|
+
toolHandlers: () => ({ do_thing: () => __awaiter(void 0, void 0, void 0, function* () { return 'ok'; }) }),
|
|
786
|
+
});
|
|
787
|
+
// A single response that emits reasoning + interstitial narration + a tool call, carrying that
|
|
788
|
+
// response's usage. The driver splits it into three messages — a `reasoning` bubble, a
|
|
789
|
+
// `narration` bubble, and the tool-call message — but only ONE may carry the cost/tokens or the
|
|
790
|
+
// session totals double-count this one response (GENC-1410).
|
|
791
|
+
const provider = scriptedProvider([
|
|
792
|
+
{
|
|
793
|
+
role: 'assistant',
|
|
794
|
+
content: 'Let me act.',
|
|
795
|
+
reasoning: 'Let me think.',
|
|
796
|
+
toolCalls: [{ id: 't1', name: 'do_thing', args: {} }],
|
|
797
|
+
cost: 0.05,
|
|
798
|
+
inputTokens: 1000,
|
|
799
|
+
outputTokens: 50,
|
|
800
|
+
},
|
|
801
|
+
// Turn-ending reply (no tool calls, no usage) so the loop terminates cleanly.
|
|
802
|
+
{ role: 'assistant', content: 'Done.' },
|
|
803
|
+
]);
|
|
804
|
+
const driver = makeDriver(config, provider);
|
|
805
|
+
yield driver.sendMessage('go');
|
|
806
|
+
const history = driver.getHistory();
|
|
807
|
+
const reasoning = history.find((m) => m.category === 'reasoning');
|
|
808
|
+
const narration = history.find((m) => m.category === 'narration');
|
|
809
|
+
const toolCallMsg = history.find((m) => { var _a; return (_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.some((c) => c.name === 'do_thing'); });
|
|
810
|
+
// The one response was split into reasoning + narration bubbles + the tool-call message.
|
|
811
|
+
assert.ok(reasoning, 'a reasoning bubble is emitted for the chain-of-thought summary');
|
|
812
|
+
assert.is(reasoning.content, 'Let me think.');
|
|
813
|
+
assert.ok(narration, 'a narration bubble is emitted for the interstitial prose');
|
|
814
|
+
assert.is(narration.content, 'Let me act.');
|
|
815
|
+
assert.ok(toolCallMsg, 'the tool call is on its own message');
|
|
816
|
+
assert.is(toolCallMsg.content, '');
|
|
817
|
+
// Only the tool-call message carries the turn's usage; the display bubbles carry none.
|
|
818
|
+
for (const m of [reasoning, narration]) {
|
|
819
|
+
assert.is(m.cost, undefined, 'a display bubble must not carry cost');
|
|
820
|
+
assert.is(m.inputTokens, undefined, 'nor inputTokens');
|
|
821
|
+
assert.is(m.outputTokens, undefined, 'nor outputTokens');
|
|
822
|
+
}
|
|
823
|
+
assert.is(toolCallMsg.cost, 0.05, 'the tool-call message carries the cost');
|
|
824
|
+
assert.is(toolCallMsg.inputTokens, 1000);
|
|
825
|
+
assert.is(toolCallMsg.outputTokens, 50);
|
|
826
|
+
// The reasoning/narration bubbles are UI-only; they must not carry the tool call either.
|
|
827
|
+
assert.is(reasoning.toolCalls, undefined, 'the reasoning bubble carries no tool call');
|
|
828
|
+
assert.is(narration.toolCalls, undefined, 'the narration bubble carries no tool call');
|
|
829
|
+
// The session totals count the one response once, not twice.
|
|
830
|
+
assert.is(sumCosts(history), 0.05, 'cost is summed once');
|
|
831
|
+
assert.is(sumTokens(history), 1050, 'tokens are summed once');
|
|
832
|
+
}));
|
|
833
|
+
costAttribution.run();
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
776
835
|
// per-agent / per-state temperature & tool-call mode (GENC-1321)
|
|
777
836
|
//
|
|
778
837
|
// The driver resolves `temperature` and `toolChoice` the same way it resolves
|
|
@@ -972,6 +1031,51 @@ interactionPresentation('presentation is absent when the option is omitted', ()
|
|
|
972
1031
|
}));
|
|
973
1032
|
interactionPresentation.run();
|
|
974
1033
|
// ---------------------------------------------------------------------------
|
|
1034
|
+
// single-instance interaction ownership (GENC-1410 Error 3) — when several views
|
|
1035
|
+
// share one driver, each interaction's widget is rendered/run by exactly one host.
|
|
1036
|
+
// The driver elects the owner (most-recently-active host, deterministic fallback),
|
|
1037
|
+
// stamps `ownerHostId` on every interaction, and de-dupes execution via a
|
|
1038
|
+
// first-writer claim.
|
|
1039
|
+
// ---------------------------------------------------------------------------
|
|
1040
|
+
const ownedInteraction = createLogicSuite('ChatDriver single-instance interaction ownership');
|
|
1041
|
+
ownedInteraction('stamps ownerHostId = most-recently-active host on the interaction', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1042
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1043
|
+
driver.registerHost('host-a');
|
|
1044
|
+
driver.registerHost('host-b');
|
|
1045
|
+
driver.setActiveHost('host-b');
|
|
1046
|
+
const pending = driver.requestInteraction('w', {});
|
|
1047
|
+
const interaction = driver.getHistory().at(-1).interaction;
|
|
1048
|
+
assert.is(interaction.ownerHostId, 'host-b', 'owner is the most-recently-active host');
|
|
1049
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1050
|
+
yield pending;
|
|
1051
|
+
}));
|
|
1052
|
+
ownedInteraction('owner falls back to the first-registered host when none is active yet', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1053
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1054
|
+
driver.registerHost('host-a');
|
|
1055
|
+
driver.registerHost('host-b');
|
|
1056
|
+
// No setActiveHost — startup, before the user has engaged a view.
|
|
1057
|
+
const pending = driver.requestInteraction('w', {});
|
|
1058
|
+
const interaction = driver.getHistory().at(-1).interaction;
|
|
1059
|
+
assert.is(interaction.ownerHostId, 'host-a', 'deterministic default = first registered');
|
|
1060
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1061
|
+
yield pending;
|
|
1062
|
+
}));
|
|
1063
|
+
ownedInteraction('no host registered → no owner (widget renders everywhere, fail-open)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1064
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1065
|
+
const pending = driver.requestInteraction('w', {});
|
|
1066
|
+
const interaction = driver.getHistory().at(-1).interaction;
|
|
1067
|
+
assert.ok(!('ownerHostId' in interaction), 'no owner stamped when no host is registered');
|
|
1068
|
+
driver.resolveInteraction(interaction.interactionId, { status: 'approved' });
|
|
1069
|
+
yield pending;
|
|
1070
|
+
}));
|
|
1071
|
+
ownedInteraction('claimSideEffect is first-writer-wins, and re-entrant for the same host', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1072
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1073
|
+
assert.is(driver.claimSideEffect('i1', 'host-a'), true, 'first host claims it');
|
|
1074
|
+
assert.is(driver.claimSideEffect('i1', 'host-b'), false, 'a different host is denied');
|
|
1075
|
+
assert.is(driver.claimSideEffect('i1', 'host-a'), true, 're-claim by the owner (re-render) is fine');
|
|
1076
|
+
}));
|
|
1077
|
+
ownedInteraction.run();
|
|
1078
|
+
// ---------------------------------------------------------------------------
|
|
975
1079
|
// interaction activity-bus signals (GENC-1346) — the driver brackets a parked
|
|
976
1080
|
// widget interaction with `interaction-requested` / `interaction-resolved`, so
|
|
977
1081
|
// turn-aware UI can distinguish "actively computing" from "parked awaiting the
|
|
@@ -18,6 +18,13 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
|
|
|
18
18
|
this.interactionId = '';
|
|
19
19
|
/** The resolved result once the interaction has completed. Forwarded to the rendered component. @internal */
|
|
20
20
|
this.resolved = undefined;
|
|
21
|
+
/**
|
|
22
|
+
* GENC-1410 (Error 3): id of the host the driver elected to render (and thus run) this
|
|
23
|
+
* interaction. When several views share one driver, only the matching host renders the live
|
|
24
|
+
* widget; others show a passive placeholder so the widget fires once. Empty = no owner elected
|
|
25
|
+
* (single view, or none registered) → render everywhere (fail-open). @internal
|
|
26
|
+
*/
|
|
27
|
+
this.ownerHostId = '';
|
|
21
28
|
}
|
|
22
29
|
connectedCallback() {
|
|
23
30
|
super.connectedCallback();
|
|
@@ -46,11 +53,19 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
|
|
|
46
53
|
this.renderComponent();
|
|
47
54
|
}
|
|
48
55
|
resolvedChanged() {
|
|
49
|
-
var _a;
|
|
56
|
+
var _a, _b;
|
|
50
57
|
const element = (_a = this.container) === null || _a === void 0 ? void 0 : _a.firstElementChild;
|
|
51
|
-
|
|
58
|
+
// GENC-1410 (Error 3): a non-owner view rendered a passive placeholder for the unresolved
|
|
59
|
+
// exclusive widget. Now it's resolved (no side effect left to fire), so render the real widget
|
|
60
|
+
// — the gate below is keyed on `!resolved`, so this instantiates the resolved widget and every
|
|
61
|
+
// view shows the outcome. The owner already has the live widget: just forward `resolved` to it,
|
|
62
|
+
// preserving its DOM/state (no re-create).
|
|
63
|
+
if (element && ((_b = element.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === this.componentName) {
|
|
52
64
|
element.resolved = this.resolved;
|
|
53
65
|
}
|
|
66
|
+
else if (this.resolved) {
|
|
67
|
+
this.renderComponent();
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
/**
|
|
56
71
|
* Watch the rendered widget element for box-size changes. When the widget
|
|
@@ -94,17 +109,41 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
|
|
|
94
109
|
return;
|
|
95
110
|
}
|
|
96
111
|
try {
|
|
112
|
+
// Pierce the wrapper's shadow boundary to read state from the assistant host.
|
|
113
|
+
const root = this.getRootNode();
|
|
114
|
+
const host = root instanceof ShadowRoot
|
|
115
|
+
? root.host
|
|
116
|
+
: null;
|
|
117
|
+
// GENC-1410 (Error 3): a widget must run on exactly ONE view. When several views share one
|
|
118
|
+
// driver, render the live widget only if this host is the elected owner AND wins the
|
|
119
|
+
// first-writer claim; otherwise show a passive placeholder and do NOT instantiate the widget
|
|
120
|
+
// (whose connectedCallback would fire a duplicate server call). A single-view app has no owner
|
|
121
|
+
// (or owns it) so it always renders. Resolved interactions always render (display only, no
|
|
122
|
+
// side effect) so state survives a pop.
|
|
123
|
+
if (!this.resolved) {
|
|
124
|
+
const isOwner = !this.ownerHostId || (host === null || host === void 0 ? void 0 : host.hostId) === this.ownerHostId;
|
|
125
|
+
// Only the elected owner attempts the claim. A non-owner must NEVER touch the latch: both
|
|
126
|
+
// views render the same history, so if a non-owner view (e.g. the hidden bubble while the
|
|
127
|
+
// panel is docked) renders its wrapper first, claiming here would grab the first-writer latch
|
|
128
|
+
// and the owner's own claim would then return false — leaving BOTH views on the placeholder,
|
|
129
|
+
// so the widget's side effect never fires (the interaction shows blank and hangs forever).
|
|
130
|
+
// Gating the claim behind `isOwner` makes the outcome independent of inter-host render order.
|
|
131
|
+
const claimed = isOwner && ((host === null || host === void 0 ? void 0 : host.claimSideEffect) ? host.claimSideEffect(this.interactionId) : true);
|
|
132
|
+
if (!claimed) {
|
|
133
|
+
const placeholder = document.createElement('div');
|
|
134
|
+
placeholder.className = 'interaction-running-elsewhere';
|
|
135
|
+
this.container.appendChild(placeholder);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
97
139
|
const element = document.createElement(this.componentName);
|
|
98
140
|
// Pass data and resolved state to the component
|
|
99
141
|
if (this.data) {
|
|
100
142
|
element.data = this.data;
|
|
101
143
|
}
|
|
102
144
|
element.resolved = this.resolved;
|
|
103
|
-
//
|
|
104
|
-
// assistant host. False if the widget is already resolved — re-renders
|
|
145
|
+
// Engagement gates autofocus. False if the widget is already resolved — re-renders
|
|
105
146
|
// from history (layout remount, dock/undock) must never steal focus.
|
|
106
|
-
const root = this.getRootNode();
|
|
107
|
-
const host = root instanceof ShadowRoot ? root.host : null;
|
|
108
147
|
element.shouldAutoFocus = !this.resolved && !!(host === null || host === void 0 ? void 0 : host.isEngaged);
|
|
109
148
|
// Handle completion from the inner component.
|
|
110
149
|
// Guard against re-emission if the interaction was already resolved
|
|
@@ -138,6 +177,9 @@ __decorate([
|
|
|
138
177
|
__decorate([
|
|
139
178
|
observable
|
|
140
179
|
], AiChatInteractionWrapper.prototype, "resolved", void 0);
|
|
180
|
+
__decorate([
|
|
181
|
+
observable
|
|
182
|
+
], AiChatInteractionWrapper.prototype, "ownerHostId", void 0);
|
|
141
183
|
AiChatInteractionWrapper = __decorate([
|
|
142
184
|
customElement({
|
|
143
185
|
name: 'ai-chat-interaction-wrapper',
|
|
@@ -74,4 +74,66 @@ Suite('forwards resolved in place without rebuilding the widget', (_a) => __awai
|
|
|
74
74
|
assert.is(element.container.firstElementChild, widget, 'resolving in place must not rebuild the widget');
|
|
75
75
|
assert.equal(widget.resolved, { status: 'approved' }, 'resolved is forwarded to the widget');
|
|
76
76
|
}));
|
|
77
|
+
/**
|
|
78
|
+
* GENC-1410 (Error 3): two assistant views (a floating bubble and a docked panel)
|
|
79
|
+
* share one driver and render the same history, so both mount a wrapper for the
|
|
80
|
+
* same interaction. Exactly one — the elected owner — must render the live widget
|
|
81
|
+
* and win the driver's first-writer side-effect latch; the other renders a passive
|
|
82
|
+
* placeholder. Regression: the claim used to be computed for EVERY host, so if a
|
|
83
|
+
* non-owner view rendered its wrapper first it grabbed the latch, and the owner's
|
|
84
|
+
* own claim then returned false — leaving BOTH views on the placeholder so the
|
|
85
|
+
* widget's side effect never fired (blank + stuck). The claim must be gated behind
|
|
86
|
+
* ownership, making the outcome independent of inter-host render order.
|
|
87
|
+
*/
|
|
88
|
+
/** First-writer-wins latch, mirroring `ChatDriver.claimSideEffect`. */
|
|
89
|
+
function makeSharedLatch() {
|
|
90
|
+
const started = new Map();
|
|
91
|
+
return (interactionId, hostId) => {
|
|
92
|
+
const existing = started.get(interactionId);
|
|
93
|
+
if (existing === undefined) {
|
|
94
|
+
started.set(interactionId, hostId);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return existing === hostId;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mount a wrapper inside a shadow host that exposes `hostId` + a shared claim,
|
|
102
|
+
* mimicking one `gc-assistant` view. The wrapper reads these across its shadow
|
|
103
|
+
* boundary via `getRootNode().host`, so a real ShadowRoot host is required.
|
|
104
|
+
*/
|
|
105
|
+
function mountWrapperInHost(hostId, ownerHostId, interactionId, claim) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
const hostEl = document.createElement('div');
|
|
108
|
+
const shadow = hostEl.attachShadow({ mode: 'open' });
|
|
109
|
+
hostEl.hostId = hostId;
|
|
110
|
+
hostEl.isEngaged = true;
|
|
111
|
+
hostEl.claimSideEffect = (id) => claim(id, hostId);
|
|
112
|
+
const wrapper = document.createElement('ai-chat-interaction-wrapper');
|
|
113
|
+
shadow.appendChild(wrapper);
|
|
114
|
+
document.body.appendChild(hostEl);
|
|
115
|
+
yield DOM.nextUpdate();
|
|
116
|
+
wrapper.ownerHostId = ownerHostId;
|
|
117
|
+
wrapper.data = { label: 'q' };
|
|
118
|
+
wrapper.interactionId = interactionId;
|
|
119
|
+
wrapper.componentName = FAKE_WIDGET;
|
|
120
|
+
yield DOM.nextUpdate();
|
|
121
|
+
return { hostEl, wrapper };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
Suite('a non-owner host does not poison the side-effect claim when it renders first', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
125
|
+
var _a, _b, _c;
|
|
126
|
+
const claim = makeSharedLatch();
|
|
127
|
+
const ownerId = 'owner-host';
|
|
128
|
+
const otherId = 'other-host';
|
|
129
|
+
const interactionId = 'id-x';
|
|
130
|
+
// The NON-owner (e.g. the hidden bubble) renders FIRST — the race that used to poison the latch.
|
|
131
|
+
const other = yield mountWrapperInHost(otherId, ownerId, interactionId, claim);
|
|
132
|
+
// The elected owner renders SECOND.
|
|
133
|
+
const owner = yield mountWrapperInHost(ownerId, ownerId, interactionId, claim);
|
|
134
|
+
assert.equal((_a = other.wrapper.container.firstElementChild) === null || _a === void 0 ? void 0 : _a.className, 'interaction-running-elsewhere', 'the non-owner view renders the passive placeholder');
|
|
135
|
+
assert.equal((_c = (_b = owner.wrapper.container.firstElementChild) === null || _b === void 0 ? void 0 : _b.tagName) === null || _c === void 0 ? void 0 : _c.toLowerCase(), FAKE_WIDGET, 'the owner renders the live widget even though the non-owner rendered first');
|
|
136
|
+
other.hostEl.remove();
|
|
137
|
+
owner.hostEl.remove();
|
|
138
|
+
}));
|
|
77
139
|
Suite.run();
|
|
@@ -20,6 +20,18 @@ const EDGE = 0.16;
|
|
|
20
20
|
const MORPH_SPEED = 3;
|
|
21
21
|
/** Global multiplier on the harmonic frequencies — raise for narrower peaks. */
|
|
22
22
|
const FREQ_SCALE = 2;
|
|
23
|
+
/** Milliseconds in one second. */
|
|
24
|
+
const MS_PER_SECOND = 1000;
|
|
25
|
+
/** Baseline frame rate (fps) the harmonic speeds are tuned against. */
|
|
26
|
+
const BASELINE_FPS = 60;
|
|
27
|
+
/**
|
|
28
|
+
* Frame duration (ms) at the baseline rate. The morph is driven by elapsed
|
|
29
|
+
* wall-clock time divided by this — not by the rAF callback count — so it
|
|
30
|
+
* advances at a constant real-world rate regardless of the display's refresh
|
|
31
|
+
* rate or dropped frames. At the baseline rate the resulting frame index
|
|
32
|
+
* matches the old per-callback counter, preserving the current look.
|
|
33
|
+
*/
|
|
34
|
+
const FRAME_MS = MS_PER_SECOND / BASELINE_FPS;
|
|
23
35
|
/**
|
|
24
36
|
* Half-thickness (viewBox units) each band keeps even between peaks, so the
|
|
25
37
|
* waves emerge from a continuous line rather than leaving gaps. The four bands
|
|
@@ -88,13 +100,6 @@ const bandLayersMarkup = BANDS.map((_b, i) => `<svg class="layer band-layer" vie
|
|
|
88
100
|
* @beta
|
|
89
101
|
*/
|
|
90
102
|
let AiFlowingWavesIndicator = AiFlowingWavesIndicator_1 = class AiFlowingWavesIndicator extends GenesisElement {
|
|
91
|
-
constructor() {
|
|
92
|
-
// A rAF loop morphs the band shapes: pure CSS can't continuously reshape the
|
|
93
|
-
// bumps (different sizes appearing at different times), and the per-frame sine
|
|
94
|
-
// sums are cheap. Same approach as the sine-tracing waves indicator.
|
|
95
|
-
super(...arguments);
|
|
96
|
-
this.frame = 0;
|
|
97
|
-
}
|
|
98
103
|
connectedCallback() {
|
|
99
104
|
super.connectedCallback();
|
|
100
105
|
// Guard against a reconnect starting a second concurrent loop.
|
|
@@ -108,8 +113,10 @@ let AiFlowingWavesIndicator = AiFlowingWavesIndicator_1 = class AiFlowingWavesIn
|
|
|
108
113
|
cancelAnimationFrame(this.animFrame);
|
|
109
114
|
this.animFrame = undefined;
|
|
110
115
|
}
|
|
116
|
+
// Reset the clock so a later reconnect restarts the morph cleanly at frame 0.
|
|
117
|
+
this.startTime = undefined;
|
|
111
118
|
}
|
|
112
|
-
tick() {
|
|
119
|
+
tick(now) {
|
|
113
120
|
var _a, _c;
|
|
114
121
|
// Stop ticking once disconnected (belt-and-braces alongside the cancel in
|
|
115
122
|
// disconnectedCallback) so no frames are scheduled while detached.
|
|
@@ -122,13 +129,17 @@ let AiFlowingWavesIndicator = AiFlowingWavesIndicator_1 = class AiFlowingWavesIn
|
|
|
122
129
|
if (paths === null || paths === void 0 ? void 0 : paths.length)
|
|
123
130
|
this.bandPaths = Array.from(paths);
|
|
124
131
|
}
|
|
132
|
+
// Anchor to the first real rAF timestamp; the initial synchronous call (no
|
|
133
|
+
// timestamp) renders frame 0. `frame` is then elapsed time in 60fps frames.
|
|
134
|
+
if (now !== undefined && this.startTime === undefined)
|
|
135
|
+
this.startTime = now;
|
|
136
|
+
const frame = now === undefined || this.startTime === undefined ? 0 : (now - this.startTime) / FRAME_MS;
|
|
125
137
|
(_c = this.bandPaths) === null || _c === void 0 ? void 0 : _c.forEach((path, i) => {
|
|
126
138
|
const cfg = BANDS[i];
|
|
127
139
|
if (cfg)
|
|
128
|
-
path.setAttribute('d', AiFlowingWavesIndicator_1.buildBand(cfg,
|
|
140
|
+
path.setAttribute('d', AiFlowingWavesIndicator_1.buildBand(cfg, frame));
|
|
129
141
|
});
|
|
130
|
-
this.
|
|
131
|
-
this.animFrame = requestAnimationFrame(() => this.tick());
|
|
142
|
+
this.animFrame = requestAnimationFrame((t) => this.tick(t));
|
|
132
143
|
}
|
|
133
144
|
/**
|
|
134
145
|
* Trace one band for the given frame: a baseline-plus-harmonics half-thickness
|
|
@@ -6,6 +6,17 @@ const HALO_DEFAULT_SPEED = 1.5;
|
|
|
6
6
|
const HALO_DEFAULT_BORDER_SIZE = 3;
|
|
7
7
|
const HALO_DEFAULT_GLOW_OPACITY = 0.35;
|
|
8
8
|
const HALO_DEFAULT_GLOW_SPREAD = 70;
|
|
9
|
+
/** Milliseconds in one second. */
|
|
10
|
+
const MS_PER_SECOND = 1000;
|
|
11
|
+
/** Baseline frame rate (fps) the `speed` (degrees/frame) is calibrated against. */
|
|
12
|
+
const BASELINE_FPS = 60;
|
|
13
|
+
/**
|
|
14
|
+
* Frame duration (ms) at the baseline rate. Rotation advances by the elapsed
|
|
15
|
+
* wall-clock time divided by this — not a fixed step per rAF callback — so it
|
|
16
|
+
* holds a constant real-world speed regardless of the display's refresh rate or
|
|
17
|
+
* dropped frames.
|
|
18
|
+
*/
|
|
19
|
+
const FRAME_MS = MS_PER_SECOND / BASELINE_FPS;
|
|
9
20
|
/**
|
|
10
21
|
* Animated halo overlay — rotating conic-gradient border with an inward glow.
|
|
11
22
|
*
|
|
@@ -52,14 +63,27 @@ let AiHaloOverlay = AiHaloOverlay_1 = class AiHaloOverlay extends GenesisElement
|
|
|
52
63
|
super.disconnectedCallback();
|
|
53
64
|
if (this.animFrame !== undefined) {
|
|
54
65
|
cancelAnimationFrame(this.animFrame);
|
|
66
|
+
this.animFrame = undefined;
|
|
55
67
|
}
|
|
68
|
+
// Reset the clock so a later reconnect doesn't apply one huge time delta.
|
|
69
|
+
this.lastTime = undefined;
|
|
56
70
|
}
|
|
57
|
-
tick() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.
|
|
71
|
+
tick(now) {
|
|
72
|
+
// Advance by elapsed wall-clock time (expressed in 60fps-equivalent frames)
|
|
73
|
+
// rather than a fixed step per callback, so the rotation runs at a constant
|
|
74
|
+
// real-world speed regardless of the display's refresh rate or dropped
|
|
75
|
+
// frames. The first tick just seeds the clock (no delta yet).
|
|
76
|
+
if (this.lastTime !== undefined && now !== undefined) {
|
|
77
|
+
const frames = (now - this.lastTime) / FRAME_MS;
|
|
78
|
+
const step = this.direction === 'ccw' ? -this.speed : this.speed;
|
|
79
|
+
const raw = this.angle + step * frames;
|
|
80
|
+
this.angle =
|
|
81
|
+
((raw % AiHaloOverlay_1.FULL_ROTATION_DEG) + AiHaloOverlay_1.FULL_ROTATION_DEG) %
|
|
82
|
+
AiHaloOverlay_1.FULL_ROTATION_DEG;
|
|
83
|
+
this.style.setProperty('--halo-angle', `${this.angle}deg`);
|
|
84
|
+
}
|
|
85
|
+
this.lastTime = now;
|
|
86
|
+
this.animFrame = requestAnimationFrame((t) => this.tick(t));
|
|
63
87
|
}
|
|
64
88
|
};
|
|
65
89
|
// TODO: The rAF loop is fine for demos but has two drawbacks vs a pure CSS @property animation:
|
|
@@ -132,6 +132,20 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
132
132
|
isBusy() {
|
|
133
133
|
return this.chatDriver.isBusy();
|
|
134
134
|
}
|
|
135
|
+
// GENC-1410 (Error 3): forward single-instance / side-effect coordination to the inner driver,
|
|
136
|
+
// which owns the interaction history and pending map.
|
|
137
|
+
registerHost(hostId) {
|
|
138
|
+
this.chatDriver.registerHost(hostId);
|
|
139
|
+
}
|
|
140
|
+
unregisterHost(hostId) {
|
|
141
|
+
this.chatDriver.unregisterHost(hostId);
|
|
142
|
+
}
|
|
143
|
+
setActiveHost(hostId) {
|
|
144
|
+
this.chatDriver.setActiveHost(hostId);
|
|
145
|
+
}
|
|
146
|
+
claimSideEffect(interactionId, hostId) {
|
|
147
|
+
return this.chatDriver.claimSideEffect(interactionId, hostId);
|
|
148
|
+
}
|
|
135
149
|
/** Currently active provider name from the underlying ChatDriver. */
|
|
136
150
|
getActiveProviderName() {
|
|
137
151
|
return this.chatDriver.getActiveProviderName();
|
|
@@ -127,6 +127,13 @@ export const settingsModalStyles = css `
|
|
|
127
127
|
gap: calc(var(--design-unit) * 2px);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
.settings-modal-section-note {
|
|
131
|
+
margin: calc(var(--design-unit) * -2px) 0 calc(var(--design-unit) * 3px);
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
line-height: 1.5;
|
|
134
|
+
color: var(--neutral-foreground-hint);
|
|
135
|
+
}
|
|
136
|
+
|
|
130
137
|
.settings-modal-section-body {
|
|
131
138
|
display: flex;
|
|
132
139
|
flex-direction: column;
|
|
@@ -116,16 +116,12 @@ const chatBotSettingsTemplate = (switchTag, buttonTag) => html `
|
|
|
116
116
|
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showThinkingSteps) != null; }, chatBotToggleRow('Show thinking', "Reveal the assistant's reasoning as it works through your request, surfacing its intermediate thought process before it commits to a response.", switchTag, 'toggle-thinking', (x) => x.showThinkingSteps, (x, c) => {
|
|
117
117
|
x.showThinkingSteps = readSwitchChecked(c.event);
|
|
118
118
|
}))}
|
|
119
|
-
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.
|
|
119
|
+
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showNarration) != null; }, chatBotToggleRow('Show working', "Show the assistant's running commentary — the short notes it writes alongside its actions as it works, separate from its reasoning and its final answer.", switchTag, 'toggle-narration', (x) => x.showNarration, (x, c) => {
|
|
120
|
+
x.showNarration = readSwitchChecked(c.event);
|
|
121
|
+
}))}
|
|
122
|
+
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showAgentSwitchIndicator) != null; }, chatBotToggleRow('Show delegation', 'Show in the chat window when tasks are delegated to different agents.', switchTag, 'toggle-agent-switch', (x) => x.showAgentSwitchIndicator, (x, c) => {
|
|
120
123
|
x.showAgentSwitchIndicator = readSwitchChecked(c.event);
|
|
121
124
|
}))}
|
|
122
|
-
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.allowDebugDownload) === true; }, settingsToggleRow('Download agent log', "Export a structured debug timeline of this session's agent activity.", 'download-agent-log', html `
|
|
123
|
-
<${buttonTag}
|
|
124
|
-
part="download-button"
|
|
125
|
-
appearance="outline"
|
|
126
|
-
@click=${(x) => x.downloadDebugLog()}
|
|
127
|
-
>Download</${buttonTag}>
|
|
128
|
-
`))}
|
|
129
125
|
${when((x) => { var _a, _b; return ((_b = (_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.animations) === null || _b === void 0 ? void 0 : _b.userConfigurable) === true; }, settingsToggleRow('Animations', 'Choose which loading and activity indicators appear while the assistant is working.', 'toggle-animations', animationsPickerTemplate(switchTag), 'column'))}
|
|
130
126
|
`;
|
|
131
127
|
const appSettingsTemplate = (switchTag) => html `
|
|
@@ -316,10 +312,24 @@ export const SettingsModalTemplate = (designSystemPrefix) => {
|
|
|
316
312
|
${appSettingsTemplate(switchTag)}
|
|
317
313
|
</div>
|
|
318
314
|
</section>
|
|
315
|
+
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.allowDebugDownload) === true; }, html `
|
|
316
|
+
<section class="settings-modal-section" part="settings-section-agent-log">
|
|
317
|
+
<div class="settings-modal-section-body">
|
|
318
|
+
${settingsToggleRow('Download agent log', "Export a structured debug timeline of this session's agent activity.", 'download-agent-log', html `
|
|
319
|
+
<${buttonTag}
|
|
320
|
+
part="download-button"
|
|
321
|
+
appearance="outline"
|
|
322
|
+
@click=${(x) => x.downloadDebugLog()}
|
|
323
|
+
>Download</${buttonTag}>
|
|
324
|
+
`)}
|
|
325
|
+
</div>
|
|
326
|
+
</section>
|
|
327
|
+
`)}
|
|
319
328
|
<section class="settings-modal-section" part="settings-section-chat">
|
|
320
329
|
<h3 class="settings-modal-section-title">
|
|
321
330
|
<${iconTag} name="comment"></${iconTag}> AI Chat Bot Settings
|
|
322
331
|
</h3>
|
|
332
|
+
<p class="settings-modal-section-note">These options only control what you see in the chat window and do not control model behaviour.</p>
|
|
323
333
|
<div class="settings-modal-section-body">
|
|
324
334
|
${chatBotSettingsTemplate(switchTag, buttonTag)}
|
|
325
335
|
</div>
|