@genesislcap/ai-assistant 15.19.0 → 15.19.1
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/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,1119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whitespace in FAST templates
|
|
3
|
-
* ────────────────────────────
|
|
4
|
-
* The formatter adds newlines/indentation to static template string parts,
|
|
5
|
-
* which FAST renders as real text nodes. With `white-space: pre-wrap` these
|
|
6
|
-
* are visible as blank lines. To avoid this:
|
|
7
|
-
*
|
|
8
|
-
* - Apply `white-space: pre-wrap` only on leaf elements whose *content* needs
|
|
9
|
-
* it, never on containers.
|
|
10
|
-
* - Inside pre-wrap elements keep content on the same line as the opening tag:
|
|
11
|
-
* GOOD `<div>${expr}</div>` / BAD `<div>\n ${expr}\n</div>`
|
|
12
|
-
* - Extract shared or deeply-nested templates as named module-level consts to
|
|
13
|
-
* keep indentation shallow.
|
|
14
|
-
* - Chain adjacent `when()` calls as `${when(a,tA)}${when(b,tB)}` — separate
|
|
15
|
-
* lines let the formatter inject whitespace between them.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type {
|
|
19
|
-
ChatAttachment,
|
|
20
|
-
ChatMessage,
|
|
21
|
-
ChatToolCall,
|
|
22
|
-
InteractionPresentation,
|
|
23
|
-
} from '@genesislcap/foundation-ai';
|
|
24
|
-
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
25
|
-
import { classNames, html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
26
|
-
import { SettingsModalTemplate } from '../components/settings-modal';
|
|
27
|
-
import { compactionDividerLabel } from '../utils/history-transform';
|
|
28
|
-
import type { FoundationAiAssistant } from './main';
|
|
29
|
-
import { LOADING_STYLE_ANIMATIONS } from './main.types';
|
|
30
|
-
|
|
31
|
-
function unknownToolPayload(tc: ChatToolCall): string {
|
|
32
|
-
if (!isChatToolCallUnknown(tc)) return '';
|
|
33
|
-
// A stale tool was real earlier in this agent's lifetime but isn't available
|
|
34
|
-
// in the current state — distinct from a hallucinated name that never existed.
|
|
35
|
-
const lines = [
|
|
36
|
-
tc.stale ? `${tc.name} — no longer available at this step` : `${tc.name} — tool does not exist`,
|
|
37
|
-
];
|
|
38
|
-
if (tc.availableTools.length > 0) {
|
|
39
|
-
lines.push('Available tools:');
|
|
40
|
-
tc.availableTools.forEach((t) => lines.push(` • ${t}`));
|
|
41
|
-
}
|
|
42
|
-
return lines.join('\n');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const HALO_SPEED_DEFAULT = 1.5;
|
|
46
|
-
const HALO_SPEED_ORCHESTRATING = 0.4;
|
|
47
|
-
const HALO_BORDER_SIZE_DEFAULT = 3;
|
|
48
|
-
|
|
49
|
-
// Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
|
|
50
|
-
// which holds the sanitized SVG string for the default or any consumer override
|
|
51
|
-
// and is injected via `:innerHTML`. A raster `image-src` / `user-image-src`
|
|
52
|
-
// takes precedence over the inline icon. The exported `*IconSvg` string
|
|
53
|
-
// constants in `./main` let apps opt back into a specific built-in mark.
|
|
54
|
-
|
|
55
|
-
const assistantAvatarTemplate = html<ChatMessage, FoundationAiAssistant>`
|
|
56
|
-
${(m, c) => {
|
|
57
|
-
const parent = c.parent as FoundationAiAssistant;
|
|
58
|
-
return parent.imageSrc
|
|
59
|
-
? html<ChatMessage, FoundationAiAssistant>`
|
|
60
|
-
<img src="${() => parent.imageSrc}" alt="Assistant" class="avatar-img" />
|
|
61
|
-
`
|
|
62
|
-
: html<ChatMessage, FoundationAiAssistant>`
|
|
63
|
-
<span class="avatar-icon" :innerHTML="${() => parent.assistantIconSafe}"></span>
|
|
64
|
-
`;
|
|
65
|
-
}}
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
const userAvatarTemplate = html<ChatMessage, FoundationAiAssistant>`
|
|
69
|
-
${(m, c) => {
|
|
70
|
-
const parent = c.parent as FoundationAiAssistant;
|
|
71
|
-
return parent.userImageSrc
|
|
72
|
-
? html<ChatMessage, FoundationAiAssistant>`
|
|
73
|
-
<img src="${() => parent.userImageSrc}" alt="You" class="avatar-img" />
|
|
74
|
-
`
|
|
75
|
-
: html<ChatMessage, FoundationAiAssistant>`
|
|
76
|
-
<span class="avatar-icon" :innerHTML="${() => parent.userIconSafe}"></span>
|
|
77
|
-
`;
|
|
78
|
-
}}
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @beta
|
|
83
|
-
*/
|
|
84
|
-
const messageType = (m: ChatMessage): string => {
|
|
85
|
-
switch (true) {
|
|
86
|
-
case m.role === 'system-event':
|
|
87
|
-
return 'agent-switch';
|
|
88
|
-
case m.role === 'user':
|
|
89
|
-
// A synthetic-user message is a display-only echo of an interaction
|
|
90
|
-
// outcome; render it identically to a real user message.
|
|
91
|
-
case m.role === 'synthetic-user':
|
|
92
|
-
return 'user';
|
|
93
|
-
// Reasoning (chain-of-thought summary) and narration (interstitial prose the model emits
|
|
94
|
-
// alongside a tool call) both render as the "Thinking" bubble — visually set apart from a real
|
|
95
|
-
// final agent reply, even though they stay separate categories (own log entry + own toggle:
|
|
96
|
-
// `showThinkingSteps` vs `showNarration`). `thinking` is the legacy flag for pre-split sessions.
|
|
97
|
-
case m.category === 'reasoning':
|
|
98
|
-
case m.category === 'narration':
|
|
99
|
-
case !!m.thinking:
|
|
100
|
-
return 'ai-thinking';
|
|
101
|
-
case !!m.toolCalls:
|
|
102
|
-
return 'ai-function';
|
|
103
|
-
case !!m.interaction:
|
|
104
|
-
return 'interaction';
|
|
105
|
-
default:
|
|
106
|
-
return 'ai';
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const senderLabel: Record<string, string> = {
|
|
111
|
-
user: 'You',
|
|
112
|
-
'ai-thinking': 'Thinking',
|
|
113
|
-
'ai-function': 'Tool Call',
|
|
114
|
-
interaction: 'Assistant',
|
|
115
|
-
ai: 'Assistant',
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Sender label for a message. Reasoning and narration share the "Thinking" bubble, so the label is
|
|
120
|
-
* the only thing that tells them apart: narration reads "Working" (the action it narrates alongside
|
|
121
|
-
* its tool call), reasoning stays "Thinking". Everything else uses the {@link messageType} map.
|
|
122
|
-
*/
|
|
123
|
-
const senderLabelFor = (m: ChatMessage): string =>
|
|
124
|
-
m.category === 'narration' ? 'Working' : senderLabel[messageType(m)];
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Resolves how the host frames an interaction widget, defaulting to `'label'`
|
|
128
|
-
* (the historical "Assistant" label, no bubble). Callers guard on
|
|
129
|
-
* `m.interaction` first; non-interaction messages never consult this.
|
|
130
|
-
*/
|
|
131
|
-
const interactionPresentation = (m: ChatMessage): InteractionPresentation =>
|
|
132
|
-
m.interaction?.presentation ?? 'label';
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Space-separated `part` names for a message bubble, so a host can style bubbles
|
|
136
|
-
* from its own stylesheet via `::part()`. Every bubble gets `message` plus its
|
|
137
|
-
* type (`message-user`, `message-ai`, `message-ai-thinking`, `message-ai-function`,
|
|
138
|
-
* `message-interaction`). Interaction bubbles additionally carry a
|
|
139
|
-
* presentation-specific part (`message-interaction-label` / `-bare` / `-bubble`)
|
|
140
|
-
* so a host can target, say, only the bubble-framed widgets.
|
|
141
|
-
*/
|
|
142
|
-
const messageParts = (m: ChatMessage): string => {
|
|
143
|
-
const type = messageType(m);
|
|
144
|
-
return type === 'interaction'
|
|
145
|
-
? `message message-interaction message-interaction-${interactionPresentation(m)}`
|
|
146
|
-
: `message message-${type}`;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// ─── Sub-agent trace fragments ────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
const subAgentAssistantTemplate = html<ChatMessage>`
|
|
152
|
-
<div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
|
|
153
|
-
`;
|
|
154
|
-
|
|
155
|
-
const subAgentToolCallTemplate = html<ChatMessage>`
|
|
156
|
-
<div class="sub-agent-message sub-agent-tool-call">
|
|
157
|
-
${repeat(
|
|
158
|
-
(m) => m.toolCalls!,
|
|
159
|
-
html<ChatToolCall>`
|
|
160
|
-
<span class="sub-agent-tool-name">${(tc) => tc.name}</span>
|
|
161
|
-
`,
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
|
-
`;
|
|
165
|
-
|
|
166
|
-
const subAgentToolResultTemplate = html<ChatMessage>`
|
|
167
|
-
<div class="sub-agent-message sub-agent-tool-result">${(m) => m.toolResult!.content}</div>
|
|
168
|
-
`;
|
|
169
|
-
|
|
170
|
-
/** Renders one row of a sub-agent trace (assistant text, tool call, or tool result). */
|
|
171
|
-
const subAgentMessageRowTemplate = html<ChatMessage>`
|
|
172
|
-
${when(
|
|
173
|
-
(m) => m.role === 'assistant' && !m.toolCalls?.length && !!m.content,
|
|
174
|
-
subAgentAssistantTemplate,
|
|
175
|
-
)}${when((m) => !!m.toolCalls?.length, subAgentToolCallTemplate)}${when(
|
|
176
|
-
(m) => m.role === 'tool' && !!m.toolResult?.content,
|
|
177
|
-
subAgentToolResultTemplate,
|
|
178
|
-
)}
|
|
179
|
-
`;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Label for a trace block: the distinct agents that appear in it.
|
|
183
|
-
*
|
|
184
|
-
* One tool call can invoke several sub-agents — a scheduler dispatching a dependency
|
|
185
|
-
* graph, or a retry — and their conversations arrive concatenated in one trace. Naming
|
|
186
|
-
* only `[0]` would then label a block "gen_A trace" while `gen_B`'s turns sit inside
|
|
187
|
-
* it. Two agents read as "gen_A, gen_B"; beyond that it degrades to a count rather
|
|
188
|
-
* than growing an unbounded summary line.
|
|
189
|
-
*/
|
|
190
|
-
const subAgentTraceLabel = (trace: readonly ChatMessage[]): string => {
|
|
191
|
-
const names = [...new Set(trace.map((m) => m.agentName).filter(Boolean))] as string[];
|
|
192
|
-
if (names.length === 0) return 'Sub-agent trace';
|
|
193
|
-
if (names.length <= 2) return `${names.join(', ')} trace`;
|
|
194
|
-
// Owns the whole string rather than returning a fragment the template suffixes, so the noun
|
|
195
|
-
// agrees with the count — a shared ` trace` suffix rendered "3 sub-agents trace".
|
|
196
|
-
return `${names.length} sub-agent traces`;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
/** Collapsed <details> trace shown inside a tool-call card once the sub-agent finishes. */
|
|
200
|
-
const subAgentTraceTemplate = html<ChatToolCall>`
|
|
201
|
-
<details class="sub-agent-trace">
|
|
202
|
-
<summary class="sub-agent-trace-summary">
|
|
203
|
-
${(tc) => subAgentTraceLabel(tc.subAgentTrace!)}
|
|
204
|
-
</summary>
|
|
205
|
-
${repeat(
|
|
206
|
-
(tc) => tc.subAgentTrace!.filter((m) => m.role !== 'user'),
|
|
207
|
-
subAgentMessageRowTemplate,
|
|
208
|
-
)}
|
|
209
|
-
</details>
|
|
210
|
-
`;
|
|
211
|
-
|
|
212
|
-
/** Live streaming trace panel shown while a sub-agent is actively running. */
|
|
213
|
-
const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
|
|
214
|
-
<div class="live-sub-agent-trace" part="live-sub-agent-trace">
|
|
215
|
-
<span class="live-sub-agent-name">${(x) => x.liveSubAgentName ?? 'Sub-agent'}</span>
|
|
216
|
-
${repeat(
|
|
217
|
-
(x) => x.liveSubAgentTrace.filter((m) => m.role !== 'user'),
|
|
218
|
-
subAgentMessageRowTemplate,
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
`;
|
|
222
|
-
|
|
223
|
-
// The interchangeable loading indicators. These MUST be stable module-level
|
|
224
|
-
// instances: the binding that selects between them reads `enabledAnimations`,
|
|
225
|
-
// which is backed by the redux store proxy and re-evaluates on every change to
|
|
226
|
-
// the aiAssistant slice (i.e. every new message, including hidden tool-call and
|
|
227
|
-
// thinking-step messages). Returning a fresh `html` instance from that binding
|
|
228
|
-
// would make FAST tear down and rebuild the indicator DOM each time, restarting
|
|
229
|
-
// the CSS animations and rAF loops. Stable references let FAST reuse the
|
|
230
|
-
// existing view so the animation runs uninterrupted.
|
|
231
|
-
const thinkingDotsTemplate = html<FoundationAiAssistant>`
|
|
232
|
-
<div class="thinking-dots">
|
|
233
|
-
<div class="dot dot-1"></div>
|
|
234
|
-
<div class="dot dot-2"></div>
|
|
235
|
-
<div class="dot dot-3"></div>
|
|
236
|
-
<div class="dot dot-4"></div>
|
|
237
|
-
</div>
|
|
238
|
-
`;
|
|
239
|
-
|
|
240
|
-
const thinkingWavesTemplate = html<FoundationAiAssistant>`
|
|
241
|
-
<div class="thinking-waves" part="thinking-waves">
|
|
242
|
-
<ai-waves-indicator></ai-waves-indicator>
|
|
243
|
-
<span class="thinking-caption">Thinking...</span>
|
|
244
|
-
</div>
|
|
245
|
-
`;
|
|
246
|
-
|
|
247
|
-
const thinkingFlowingWavesTemplate = html<FoundationAiAssistant>`
|
|
248
|
-
<div class="thinking-flowing-waves" part="thinking-flowing-waves">
|
|
249
|
-
<ai-flowing-waves-indicator></ai-flowing-waves-indicator>
|
|
250
|
-
<span class="thinking-caption">Thinking...</span>
|
|
251
|
-
</div>
|
|
252
|
-
`;
|
|
253
|
-
|
|
254
|
-
const thinkingPlasmaTemplate = html<FoundationAiAssistant>`
|
|
255
|
-
<div class="thinking-plasma" part="thinking-plasma">
|
|
256
|
-
<ai-plasma-orb-indicator></ai-plasma-orb-indicator>
|
|
257
|
-
<span class="thinking-caption">Thinking...</span>
|
|
258
|
-
</div>
|
|
259
|
-
`;
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Picks the loading indicator for the currently enabled style, falling back to
|
|
263
|
-
* the dots (also the default when no animations config is supplied). Returns a
|
|
264
|
-
* stable template instance per style — see the note above.
|
|
265
|
-
*/
|
|
266
|
-
const selectThinkingTemplate = (x: FoundationAiAssistant): ViewTemplate<FoundationAiAssistant> => {
|
|
267
|
-
const enabled = x.enabledAnimations;
|
|
268
|
-
if (enabled.includes('waves')) return thinkingWavesTemplate;
|
|
269
|
-
if (enabled.includes('flowingWaves')) return thinkingFlowingWavesTemplate;
|
|
270
|
-
if (enabled.includes('plasma')) return thinkingPlasmaTemplate;
|
|
271
|
-
return thinkingDotsTemplate;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/** Shown in place of the transcript while a manual compaction runs (GENC-1351 §5.7). */
|
|
275
|
-
const compactingTemplate = html<FoundationAiAssistant>`
|
|
276
|
-
<div class="compacting-indicator" part="compacting-indicator">
|
|
277
|
-
${thinkingDotsTemplate}
|
|
278
|
-
<span class="compacting-caption">Compacting…</span>
|
|
279
|
-
</div>
|
|
280
|
-
`;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Shown in place of the transcript while a page-reload restore is in flight
|
|
284
|
-
* (GENC-1351 §6) — a slow, server-backed `provider.load()`. Reuses the compaction
|
|
285
|
-
* indicator's visual (same centered dotted loader) with its own `part` for host
|
|
286
|
-
* styling. Invisible for synchronous providers, which resolve before paint.
|
|
287
|
-
*/
|
|
288
|
-
const restoringTemplate = html<FoundationAiAssistant>`
|
|
289
|
-
<div class="compacting-indicator" part="restoring-indicator">
|
|
290
|
-
${thinkingDotsTemplate}
|
|
291
|
-
<span class="compacting-caption">Restoring…</span>
|
|
292
|
-
</div>
|
|
293
|
-
`;
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Class list for the banner below, joined rather than interpolated so an
|
|
297
|
-
* inapplicable modifier contributes nothing. Two interpolations directly in the
|
|
298
|
-
* attribute emitted `class="blocked-banner "` in the common (unblocked) case —
|
|
299
|
-
* harmless to the browser, but it shows up in every DOM snapshot and every
|
|
300
|
-
* innerHTML assertion a host writes against this element.
|
|
301
|
-
*
|
|
302
|
-
* The two modifiers name what they actually gate, which is why neither is
|
|
303
|
-
* `is-blocked`: `is-visible` means the banner has something to say, and that
|
|
304
|
-
* includes PARTIAL exhaustion — one vendor walled, composer still live, `blocked`
|
|
305
|
-
* false. `is-partial` then softens the treatment for exactly that case. Naming
|
|
306
|
-
* the first after `blocked` read as a contradiction beside the second, and made
|
|
307
|
-
* the styles say `.blocked-banner.is-blocked` to mean "visible".
|
|
308
|
-
*
|
|
309
|
-
* Exported for the unit test that pins the attribute; not part of the element
|
|
310
|
-
* API.
|
|
311
|
-
*
|
|
312
|
-
* @internal
|
|
313
|
-
*/
|
|
314
|
-
export const blockedBannerClasses = (x: FoundationAiAssistant): string =>
|
|
315
|
-
[
|
|
316
|
-
'blocked-banner',
|
|
317
|
-
x.bannerVisible ? 'is-visible' : '',
|
|
318
|
-
x.bannerVisible && !x.blocked ? 'is-partial' : '',
|
|
319
|
-
]
|
|
320
|
-
.filter(Boolean)
|
|
321
|
-
.join(' ');
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Id of the blocked banner, referenced by the composer controls'
|
|
325
|
-
* `aria-describedby`. Shadow-DOM-scoped, so a fixed string cannot collide with
|
|
326
|
-
* the host page — and IDREF resolution is same-root, which is exactly where both
|
|
327
|
-
* ends of this reference live.
|
|
328
|
-
*
|
|
329
|
-
* @internal
|
|
330
|
-
*/
|
|
331
|
-
export const BLOCKED_BANNER_ID = 'blocked-banner';
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* `aria-describedby` for the composer's textarea, send button and attach button:
|
|
335
|
-
* the banner's id whenever the banner has something to say, otherwise `null` (so
|
|
336
|
-
* the attribute is omitted rather than emitted empty).
|
|
337
|
-
*
|
|
338
|
-
* Keyed on `bannerVisible`, NOT on `blocked`, and that is the point. The
|
|
339
|
-
* PARTIAL state — some vendor walled, composer still live — is the state this
|
|
340
|
-
* feature exists to create, and it was the one state with no accessible
|
|
341
|
-
* explanation at all: `aria-disabled` and `aria-label` bind only on `blocked`, and
|
|
342
|
-
* a live composer keeps the host's own placeholder, so a screen-reader user
|
|
343
|
-
* arriving at the textarea heard "Type a message" with no hint that the next turn
|
|
344
|
-
* might be refused. The banner's `role="status"` announces the text when it
|
|
345
|
-
* CHANGES; this is what makes the same explanation reachable afterwards, on
|
|
346
|
-
* demand, from the control it is about.
|
|
347
|
-
*
|
|
348
|
-
* Applied in the fully blocked state too, where it is additive: the `aria-label`
|
|
349
|
-
* there states the reason as the control's name, and this restates it as its
|
|
350
|
-
* description for the send/attach buttons, which carry neither.
|
|
351
|
-
*
|
|
352
|
-
* Exported for the unit test that pins it; not part of the element API.
|
|
353
|
-
*
|
|
354
|
-
* @internal
|
|
355
|
-
*/
|
|
356
|
-
export const composerDescribedBy = (x: FoundationAiAssistant): string | null =>
|
|
357
|
-
x.bannerVisible ? BLOCKED_BANNER_ID : null;
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Persistent banner shown above the composer while a backend condition blocks
|
|
361
|
-
* the assistant — today an exhausted AI budget (GENC-1464).
|
|
362
|
-
*
|
|
363
|
-
* Deliberately NOT a transcript replacement like `compactingTemplate` /
|
|
364
|
-
* `restoringTemplate`: those cover a transient rewrite where the history is
|
|
365
|
-
* mid-flight and unsafe to show, whereas a block is indefinite. Hiding the
|
|
366
|
-
* conversation behind it would strand the user's work behind a wall they
|
|
367
|
-
* cannot clear, so the banner sits alongside a fully readable transcript.
|
|
368
|
-
*
|
|
369
|
-
* `role="status"` (not `alert`) — it is a standing condition the user can also
|
|
370
|
-
* see in the disabled composer, not an interruption.
|
|
371
|
-
*
|
|
372
|
-
* **Rendered unconditionally, empty when unblocked.** This is an accessibility
|
|
373
|
-
* requirement, not a stylistic choice: `role="status"` announces MUTATIONS to a
|
|
374
|
-
* region the assistive technology was already observing. A region INSERTED into
|
|
375
|
-
* the DOM already carrying its text is widely missed across NVDA, JAWS and
|
|
376
|
-
* VoiceOver — and it is the only announcement a screen-reader user gets, since
|
|
377
|
-
* the textarea simultaneously flips to `disabled`, which drops it out of the tab
|
|
378
|
-
* order and means the swapped "AI usage limit reached" placeholder is never
|
|
379
|
-
* voiced (`maybeAutoFocusChatInput` also no-ops against a disabled control). So
|
|
380
|
-
* the region stays mounted and only its TEXT changes.
|
|
381
|
-
*
|
|
382
|
-
* It is deliberately **not** collapsed with `display: none` when unblocked —
|
|
383
|
-
* that would drop it out of the accessibility tree and make the flip an
|
|
384
|
-
* insertion again, defeating the point. Instead the visual treatment (padding,
|
|
385
|
-
* border, background) hangs off the `is-visible` class, so the empty container
|
|
386
|
-
* occupies no space while remaining a live, registered region.
|
|
387
|
-
*
|
|
388
|
-
* It carries {@link BLOCKED_BANNER_ID} so the composer controls can point
|
|
389
|
-
* `aria-describedby` at it — see {@link composerDescribedBy}.
|
|
390
|
-
*/
|
|
391
|
-
const blockedBannerTemplate = html<FoundationAiAssistant>`
|
|
392
|
-
<div
|
|
393
|
-
id="${BLOCKED_BANNER_ID}"
|
|
394
|
-
class="${blockedBannerClasses}"
|
|
395
|
-
part="blocked-banner"
|
|
396
|
-
role="status"
|
|
397
|
-
>
|
|
398
|
-
${when(
|
|
399
|
-
(x) => x.bannerVisible,
|
|
400
|
-
html<FoundationAiAssistant>`
|
|
401
|
-
<span class="blocked-banner-text">${(x) => x.effectiveBlockedReason}</span>
|
|
402
|
-
`,
|
|
403
|
-
)}
|
|
404
|
-
</div>
|
|
405
|
-
`;
|
|
406
|
-
|
|
407
|
-
// ─── Public factory ───────────────────────────────────────────────────────────
|
|
408
|
-
|
|
409
|
-
/** @internal */
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* GENC-1511 — blocking "not saving" modal.
|
|
413
|
-
*
|
|
414
|
-
* Deliberately has NO way out but Reload. The failure it reports is invisible by nature — a
|
|
415
|
-
* prod session wrote nothing for 3h17m with a normal-looking UI throughout — and anything the
|
|
416
|
-
* user can dismiss is an invitation to carry on producing work that will be lost. So:
|
|
417
|
-
* `show-close-icon` off, our own `bottom` slot replacing the default close button, and the
|
|
418
|
-
* native dialog's Escape route cancelled on `mainElement` in `persistenceBrokenChanged`.
|
|
419
|
-
*
|
|
420
|
-
* It is only ever shown at a point where reloading is SAFE — the persister gates on the active
|
|
421
|
-
* agent being resumable — so the advice cannot cost the user in-flight work it is unable to carry.
|
|
422
|
-
*
|
|
423
|
-
* ⚠ `showCloseIcon` is set as a PROPERTY (`:showCloseIcon`), not via the `?show-close-icon`
|
|
424
|
-
* boolean attribute. It is an `@attr({ mode: 'boolean' })` whose FIELD default is `true`, so the
|
|
425
|
-
* property reflects the attribute ON at construction and the boolean binding's "absent means
|
|
426
|
-
* false" never wins. Measured live 2026-08-20: the close icon rendered anyway
|
|
427
|
-
* (`showCloseIcon=true`, `hasAttribute=true`) until the property was set directly. Do not
|
|
428
|
-
* "simplify" it back to `?show-close-icon` — this modal must not be dismissible.
|
|
429
|
-
*/
|
|
430
|
-
const persistenceBrokenModalTemplate = (
|
|
431
|
-
designSystemPrefix: string,
|
|
432
|
-
): ViewTemplate<FoundationAiAssistant> => {
|
|
433
|
-
const modalTag = `${designSystemPrefix}-modal`;
|
|
434
|
-
const buttonTag = `${designSystemPrefix}-button`;
|
|
435
|
-
const iconTag = `${designSystemPrefix}-icon`;
|
|
436
|
-
return html<FoundationAiAssistant>`
|
|
437
|
-
<${modalTag}
|
|
438
|
-
class="persistence-broken-modal"
|
|
439
|
-
part="persistence-broken-modal"
|
|
440
|
-
${ref('persistenceModal')}
|
|
441
|
-
initial-width="480"
|
|
442
|
-
:showCloseIcon=${() => false}
|
|
443
|
-
>
|
|
444
|
-
<div slot="top" class="persistence-broken-header" part="persistence-broken-header">
|
|
445
|
-
<h2 class="persistence-broken-title">
|
|
446
|
-
<${iconTag} name="triangle-exclamation"></${iconTag}> Your work isn't being saved
|
|
447
|
-
</h2>
|
|
448
|
-
</div>
|
|
449
|
-
<div class="persistence-broken-content" part="persistence-broken-content">
|
|
450
|
-
<p>
|
|
451
|
-
${(x) =>
|
|
452
|
-
x.persistenceBrokenMessage ??
|
|
453
|
-
"We've encountered a problem and this session has stopped saving to the server. " +
|
|
454
|
-
'Anything you do from here will be lost, so please reload before carrying on.'}
|
|
455
|
-
</p>
|
|
456
|
-
<!--
|
|
457
|
-
Quantified where the reporter can manage it, because "some work" is not actionable.
|
|
458
|
-
Composed by the CALLER, not here: the assistant's persister counts unsaved messages, a
|
|
459
|
-
host counting something else has its own sentence, and a template that knew about
|
|
460
|
-
"messages" could only ever serve the first. Omitted entirely when nobody supplied one.
|
|
461
|
-
-->
|
|
462
|
-
${when(
|
|
463
|
-
(x) => !!x.persistenceBrokenDetail,
|
|
464
|
-
html<FoundationAiAssistant>`
|
|
465
|
-
<p class="persistence-broken-detail">${(x) => x.persistenceBrokenDetail}</p>
|
|
466
|
-
`,
|
|
467
|
-
)}
|
|
468
|
-
<p class="persistence-broken-detail">
|
|
469
|
-
Reloading restores the last saved point.${when(
|
|
470
|
-
(x) => x.chatConfig.ui?.allowDebugDownload === true,
|
|
471
|
-
html<FoundationAiAssistant>`
|
|
472
|
-
You can download a record of this conversation first.
|
|
473
|
-
`,
|
|
474
|
-
)}
|
|
475
|
-
</p>
|
|
476
|
-
</div>
|
|
477
|
-
<div slot="bottom" class="persistence-broken-actions" part="persistence-broken-actions">
|
|
478
|
-
<!--
|
|
479
|
-
The download lives HERE, not in Settings. The copy used to point at the Settings panel,
|
|
480
|
-
which this modal makes unreachable by design — offering a record the user cannot get to.
|
|
481
|
-
Gated on the host's own allowDebugDownload flag, same as the Settings entry, so a host
|
|
482
|
-
that turned it off does not get it smuggled back in through the failure path; the
|
|
483
|
-
sentence above is gated on the same flag so the copy never promises a missing button.
|
|
484
|
-
-->
|
|
485
|
-
${when(
|
|
486
|
-
(x) => x.chatConfig.ui?.allowDebugDownload === true,
|
|
487
|
-
html<FoundationAiAssistant>`
|
|
488
|
-
<${buttonTag}
|
|
489
|
-
appearance="outline"
|
|
490
|
-
part="persistence-broken-download"
|
|
491
|
-
?disabled=${(x) => x.downloadingDebugLog}
|
|
492
|
-
@click=${(x) => x.downloadDebugLog()}
|
|
493
|
-
>
|
|
494
|
-
${(x) => (x.downloadingDebugLog ? 'Preparing…' : 'Download agent log')}
|
|
495
|
-
</${buttonTag}>
|
|
496
|
-
`,
|
|
497
|
-
)}
|
|
498
|
-
<${buttonTag}
|
|
499
|
-
appearance="accent"
|
|
500
|
-
part="persistence-broken-reload"
|
|
501
|
-
@click=${(x) => x.reloadForPersistence()}
|
|
502
|
-
>
|
|
503
|
-
Reload the page
|
|
504
|
-
</${buttonTag}>
|
|
505
|
-
</div>
|
|
506
|
-
</${modalTag}>
|
|
507
|
-
`;
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
export const FoundationAiAssistantTemplate = (
|
|
511
|
-
designSystemPrefix: string,
|
|
512
|
-
): ViewTemplate<FoundationAiAssistant> => {
|
|
513
|
-
const buttonTag = `${designSystemPrefix}-button`;
|
|
514
|
-
const textareaTag = `${designSystemPrefix}-text-area`;
|
|
515
|
-
const iconTag = `${designSystemPrefix}-icon`;
|
|
516
|
-
|
|
517
|
-
// ── Tool call item ──────────────────────────────────────────────────────────
|
|
518
|
-
|
|
519
|
-
const toolCallItemTemplate = html<ChatToolCall>`
|
|
520
|
-
${when(
|
|
521
|
-
(tc) => tc.foldEvent === 'open',
|
|
522
|
-
html<ChatToolCall>`
|
|
523
|
-
<pre class="payload fold-event fold-open">► ${(tc) => tc.name}</pre>
|
|
524
|
-
`,
|
|
525
|
-
)}
|
|
526
|
-
${when(
|
|
527
|
-
(tc) => tc.foldEvent === 'close',
|
|
528
|
-
html<ChatToolCall>`
|
|
529
|
-
<pre class="payload fold-event fold-close">◄ ${(tc) => tc.name}</pre>
|
|
530
|
-
`,
|
|
531
|
-
)}
|
|
532
|
-
${when(
|
|
533
|
-
(tc) => !tc.foldEvent && !isChatToolCallUnknown(tc),
|
|
534
|
-
html<ChatToolCall>`
|
|
535
|
-
<pre class="payload">
|
|
536
|
-
${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<strong>${(tc) =>
|
|
537
|
-
tc.name}</strong>(${(tc) => JSON.stringify(tc.args, null, 2)})</pre
|
|
538
|
-
>
|
|
539
|
-
`,
|
|
540
|
-
)}
|
|
541
|
-
${when(
|
|
542
|
-
isChatToolCallUnknown,
|
|
543
|
-
html<ChatToolCall>`
|
|
544
|
-
<pre class="payload unknown-tool">${(tc) => unknownToolPayload(tc)}</pre>
|
|
545
|
-
`,
|
|
546
|
-
)}
|
|
547
|
-
${when((tc) => !!tc.subAgentTrace?.length, subAgentTraceTemplate)}
|
|
548
|
-
`;
|
|
549
|
-
|
|
550
|
-
// ── Message row ─────────────────────────────────────────────────────────────
|
|
551
|
-
|
|
552
|
-
const messageRowTemplate = html<ChatMessage, FoundationAiAssistant>`
|
|
553
|
-
${when(
|
|
554
|
-
(m) => m.role === 'system-event',
|
|
555
|
-
html<ChatMessage>`
|
|
556
|
-
<div class="agent-switch-indicator" part="agent-switch-indicator">
|
|
557
|
-
<span class="agent-switch-label">${(m) => m.content}</span>
|
|
558
|
-
</div>
|
|
559
|
-
`,
|
|
560
|
-
)}
|
|
561
|
-
${when(
|
|
562
|
-
(m) => m.role === 'compacted-summary',
|
|
563
|
-
html<ChatMessage>`
|
|
564
|
-
<div class="agent-switch-indicator compaction-divider" part="compaction-divider">
|
|
565
|
-
<span class="agent-switch-label">${(m) => compactionDividerLabel(m)}</span>
|
|
566
|
-
</div>
|
|
567
|
-
`,
|
|
568
|
-
)}
|
|
569
|
-
${when(
|
|
570
|
-
(m) => m.role !== 'system-event' && m.role !== 'compacted-summary',
|
|
571
|
-
html<ChatMessage, FoundationAiAssistant>`
|
|
572
|
-
<div
|
|
573
|
-
class="message-row ${(m) => messageType(m)} ${(m) =>
|
|
574
|
-
m.interaction ? `present-${interactionPresentation(m)}` : ''}"
|
|
575
|
-
>
|
|
576
|
-
<div class="avatar ${(m) => messageType(m)}">
|
|
577
|
-
${when(
|
|
578
|
-
// Keyed on messageType (not raw role) so a synthetic-user message,
|
|
579
|
-
// which renders as 'user', gets the user avatar rather than the
|
|
580
|
-
// assistant one.
|
|
581
|
-
(m) => messageType(m) === 'user',
|
|
582
|
-
userAvatarTemplate,
|
|
583
|
-
)}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
|
|
584
|
-
</div>
|
|
585
|
-
<div
|
|
586
|
-
class="message ${(m) => messageType(m)} ${(m) =>
|
|
587
|
-
classNames(['narration', m.category === 'narration'])}"
|
|
588
|
-
part="${(m) => messageParts(m)}"
|
|
589
|
-
>
|
|
590
|
-
${when(
|
|
591
|
-
// A 'bare' interaction owns its full presentation — suppress the
|
|
592
|
-
// host sender label. Every other message keeps it.
|
|
593
|
-
(m) => !(m.interaction && interactionPresentation(m) === 'bare'),
|
|
594
|
-
html<ChatMessage, FoundationAiAssistant>`
|
|
595
|
-
<div class="sender">
|
|
596
|
-
${(m, c) =>
|
|
597
|
-
messageType(m) === 'ai-function' &&
|
|
598
|
-
m.agentName &&
|
|
599
|
-
(c.parent as FoundationAiAssistant).showAgentSwitchIndicator
|
|
600
|
-
? `Tool Call · ${m.agentLabel ?? m.agentName}`
|
|
601
|
-
: senderLabelFor(m)}
|
|
602
|
-
</div>
|
|
603
|
-
`,
|
|
604
|
-
)}
|
|
605
|
-
<div class="content">
|
|
606
|
-
${when(
|
|
607
|
-
(m) => m.content,
|
|
608
|
-
html<ChatMessage>`
|
|
609
|
-
<ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
|
|
610
|
-
`,
|
|
611
|
-
)}
|
|
612
|
-
${when(
|
|
613
|
-
(m) => m.toolCalls,
|
|
614
|
-
html<ChatMessage>`
|
|
615
|
-
${repeat((m) => m.toolCalls ?? [], toolCallItemTemplate)}
|
|
616
|
-
`,
|
|
617
|
-
)}
|
|
618
|
-
${when(
|
|
619
|
-
(m) => m.interaction,
|
|
620
|
-
html<ChatMessage, FoundationAiAssistant>`
|
|
621
|
-
<ai-chat-interaction-wrapper
|
|
622
|
-
:componentName=${(m) => m.interaction!.componentName}
|
|
623
|
-
:data=${(m) => m.interaction!.data}
|
|
624
|
-
:interactionId=${(m) => m.interaction!.interactionId}
|
|
625
|
-
:resolved=${(m) => m.interaction!.resolved}
|
|
626
|
-
@interaction-completed=${(m, c) => c.parent.handleInteractionCompleted(c.event)}
|
|
627
|
-
@interaction-cost=${(m, c) => c.parent.handleInteractionCost(c.event)}
|
|
628
|
-
></ai-chat-interaction-wrapper>
|
|
629
|
-
`,
|
|
630
|
-
)}
|
|
631
|
-
</div>
|
|
632
|
-
</div>
|
|
633
|
-
</div>
|
|
634
|
-
`,
|
|
635
|
-
)}
|
|
636
|
-
`;
|
|
637
|
-
|
|
638
|
-
// ── Composer control pills ──────────────────────────────────────────────────
|
|
639
|
-
//
|
|
640
|
-
// The three leading controls share one round-icon-button shape (`composer-pill`)
|
|
641
|
-
// and sit INSIDE the composer's box, on a bar under the textarea. The agent
|
|
642
|
-
// pill additionally carries a label, so it is the only one that grows.
|
|
643
|
-
|
|
644
|
-
const agentToggleButtonTemplate = html<FoundationAiAssistant>`
|
|
645
|
-
<${buttonTag}
|
|
646
|
-
class="composer-pill composer-pill-labelled agent-toggle-button"
|
|
647
|
-
part="agent-toggle-button"
|
|
648
|
-
appearance="stealth"
|
|
649
|
-
aria-haspopup="menu"
|
|
650
|
-
aria-expanded=${(x) => (x.agentPickerOpen ? 'true' : 'false')}
|
|
651
|
-
title=${(x) => x.agentToggleTitle}
|
|
652
|
-
?disabled=${(x) => x.busy || x.pinLocked}
|
|
653
|
-
@click=${(x) => x.toggleAgentPicker()}
|
|
654
|
-
><span
|
|
655
|
-
class="pill-glyph"
|
|
656
|
-
style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
|
|
657
|
-
><${iconTag}
|
|
658
|
-
name="${(x) => (x.pinnedAgentName === null ? 'a' : 'thumbtack')}"
|
|
659
|
-
></${iconTag}></span
|
|
660
|
-
><span class="pill-label">${(x) => x.agentButtonLabel}</span></${buttonTag}>
|
|
661
|
-
`;
|
|
662
|
-
|
|
663
|
-
// The attach button shares the textarea's disable gate. Attaching a file that
|
|
664
|
-
// can never be sent is a dead end — and `blocked`, `compacting` and `restoring`
|
|
665
|
-
// were all missing from it, so files could be attached during a compaction or
|
|
666
|
-
// against an exhausted budget and then never sent.
|
|
667
|
-
const attachButtonTemplate = html<FoundationAiAssistant>`
|
|
668
|
-
<${buttonTag}
|
|
669
|
-
class="composer-pill attach-button"
|
|
670
|
-
part="attach-button"
|
|
671
|
-
appearance="stealth"
|
|
672
|
-
title=${(x) => `Attach file (${x.chatConfig.ui?.acceptedFiles})`}
|
|
673
|
-
aria-label="Attach file"
|
|
674
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
|
|
675
|
-
aria-describedby=${composerDescribedBy}
|
|
676
|
-
@click=${(x) => x.triggerFileInput()}
|
|
677
|
-
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
678
|
-
`;
|
|
679
|
-
|
|
680
|
-
const sessionMenuButtonTemplate = html<FoundationAiAssistant>`
|
|
681
|
-
<${buttonTag}
|
|
682
|
-
class="composer-pill session-menu-button"
|
|
683
|
-
part="session-menu-button"
|
|
684
|
-
appearance="stealth"
|
|
685
|
-
title="Session options"
|
|
686
|
-
aria-label="Session options"
|
|
687
|
-
aria-haspopup="menu"
|
|
688
|
-
aria-expanded=${(x) => (x.sessionMenuOpen ? 'true' : 'false')}
|
|
689
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring}
|
|
690
|
-
@click=${(x) => x.toggleSessionMenu()}
|
|
691
|
-
><${iconTag} name="ellipsis"></${iconTag}></${buttonTag}>
|
|
692
|
-
`;
|
|
693
|
-
|
|
694
|
-
// The agent control is LAST in the cluster because it is the only one whose
|
|
695
|
-
// width changes: pinning an agent swaps `AUTO` for a name, and with it in
|
|
696
|
-
// front the fixed-size buttons would slide sideways every time the pin
|
|
697
|
-
// changed. Trailing, it grows into the empty middle of the bar instead.
|
|
698
|
-
const composerBarStartTemplate = html<FoundationAiAssistant>`
|
|
699
|
-
<div class="composer-bar-start" part="composer-bar-start">
|
|
700
|
-
${when((x) => !!x.chatConfig.ui?.acceptedFiles, attachButtonTemplate)}${when(
|
|
701
|
-
(x) => x.sessionMenuEnabled,
|
|
702
|
-
sessionMenuButtonTemplate,
|
|
703
|
-
)}${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
|
|
704
|
-
</div>
|
|
705
|
-
`;
|
|
706
|
-
|
|
707
|
-
// ── Send / Stop ─────────────────────────────────────────────────────────────
|
|
708
|
-
//
|
|
709
|
-
// One slot, two mutually exclusive actions. While a turn runs, Stop takes the
|
|
710
|
-
// slot outright rather than appearing alongside a greyed-out Send — a disabled
|
|
711
|
-
// control next to a live one is just something for the eye to skip.
|
|
712
|
-
|
|
713
|
-
const stopButtonTemplate = html<FoundationAiAssistant>`
|
|
714
|
-
<${buttonTag}
|
|
715
|
-
class="composer-pill stop-button"
|
|
716
|
-
part="stop-button"
|
|
717
|
-
appearance="stealth"
|
|
718
|
-
title="${(x) => (x.state === 'cancelling' ? 'Stopping…' : 'Stop')}"
|
|
719
|
-
aria-label="${(x) => (x.state === 'cancelling' ? 'Stopping' : 'Stop')}"
|
|
720
|
-
?disabled=${(x) => x.state === 'cancelling'}
|
|
721
|
-
@click=${(x) => x.handleStopClick()}
|
|
722
|
-
><${iconTag}
|
|
723
|
-
class="${(x) => classNames('stop-icon', ['stop-icon-spinning', x.state === 'cancelling'])}"
|
|
724
|
-
name="${(x) => (x.state === 'cancelling' ? 'spinner' : 'stop')}"
|
|
725
|
-
></${iconTag}></${buttonTag}>
|
|
726
|
-
`;
|
|
727
|
-
|
|
728
|
-
// The shell exists so a host can paint a gradient RING around Send without
|
|
729
|
-
// owning the button's own surface: it is an inert zero-padding wrapper by
|
|
730
|
-
// default, and ::part(send-shell) given a padding plus a background becomes
|
|
731
|
-
// that ring. Styling ::part(send-button) alone cannot do it — the design-system
|
|
732
|
-
// button paints its own background over any gradient set on it.
|
|
733
|
-
const sendButtonTemplate = html<FoundationAiAssistant>`
|
|
734
|
-
<span class="send-shell" part="send-shell">
|
|
735
|
-
<${buttonTag}
|
|
736
|
-
class="send-button"
|
|
737
|
-
part="send-button"
|
|
738
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked || (!x.inputValue.trim() && !x.attachments.length)}
|
|
739
|
-
aria-describedby=${composerDescribedBy}
|
|
740
|
-
@click=${(x) => x.handleSendClick()}
|
|
741
|
-
>Send</${buttonTag}>
|
|
742
|
-
</span>
|
|
743
|
-
`;
|
|
744
|
-
|
|
745
|
-
// ── Composer menus ──────────────────────────────────────────────────────────
|
|
746
|
-
//
|
|
747
|
-
// Both menus are the same object: a `composer-menu` panel absolutely
|
|
748
|
-
// positioned above the composer box, edge-aligned to the button that opened it
|
|
749
|
-
// (`-start` / `-end`) and animated from that corner so it reads as growing out
|
|
750
|
-
// of the button rather than appearing overhead. Only the contents differ — the
|
|
751
|
-
// rows themselves share `menuItemStyles` across the two shadow roots.
|
|
752
|
-
|
|
753
|
-
const agentPickerPanelTemplate = html<FoundationAiAssistant>`
|
|
754
|
-
<div class="composer-menu agent-picker-panel" part="agent-picker-panel">
|
|
755
|
-
<foundation-ai-agent-picker
|
|
756
|
-
part="agent-picker"
|
|
757
|
-
:mode="${(x) => x.agentPicker}"
|
|
758
|
-
:agents="${(x) => x.agents ?? []}"
|
|
759
|
-
:pinnedAgentName="${(x) => x.pinnedAgentName}"
|
|
760
|
-
:disablePinColours="${(x) => !!x.chatConfig.picker?.disablePinColours}"
|
|
761
|
-
@agent-pinned="${(x, c) => {
|
|
762
|
-
x.pinnedAgentName = (c.event as CustomEvent<string | null>).detail;
|
|
763
|
-
x.toggleAgentPicker();
|
|
764
|
-
}}"
|
|
765
|
-
></foundation-ai-agent-picker>
|
|
766
|
-
</div>
|
|
767
|
-
`;
|
|
768
|
-
|
|
769
|
-
// ── Session lifecycle menu (GENC-1351 §5.5) ──────────────────────────────────
|
|
770
|
-
|
|
771
|
-
// Clear and Compact both confirm in place: the row you picked is replaced by a
|
|
772
|
-
// danger-toned Confirm plus a Cancel, so the destructive step is a second
|
|
773
|
-
// deliberate click and the menu never changes size enough to move under the
|
|
774
|
-
// cursor.
|
|
775
|
-
const sessionMenuIdleTemplate = html<FoundationAiAssistant>`
|
|
776
|
-
<button
|
|
777
|
-
class="menu-item"
|
|
778
|
-
role="menuitem"
|
|
779
|
-
type="button"
|
|
780
|
-
?disabled=${(x) => !x.compactable}
|
|
781
|
-
title=${(x) =>
|
|
782
|
-
x.compactable
|
|
783
|
-
? 'Summarize older messages to free up context'
|
|
784
|
-
: 'Not enough conversation yet to be worth compacting'}
|
|
785
|
-
@click=${(x) => (x.confirmingCompact = true)}
|
|
786
|
-
>
|
|
787
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="compress"></${iconTag}></span>
|
|
788
|
-
<span class="menu-item-body">
|
|
789
|
-
<span class="menu-item-name">Compact</span>
|
|
790
|
-
<span class="menu-item-hint">Summarize older messages to free up context</span>
|
|
791
|
-
</span>
|
|
792
|
-
</button>
|
|
793
|
-
<button
|
|
794
|
-
class="menu-item"
|
|
795
|
-
role="menuitem"
|
|
796
|
-
type="button"
|
|
797
|
-
@click=${(x) => (x.confirmingClear = true)}
|
|
798
|
-
>
|
|
799
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
|
|
800
|
-
<span class="menu-item-body">
|
|
801
|
-
<span class="menu-item-name">Clear</span>
|
|
802
|
-
<span class="menu-item-hint">Start a new conversation</span>
|
|
803
|
-
</span>
|
|
804
|
-
</button>
|
|
805
|
-
`;
|
|
806
|
-
|
|
807
|
-
const sessionMenuCancelTemplate = html<FoundationAiAssistant>`
|
|
808
|
-
<button
|
|
809
|
-
class="menu-item"
|
|
810
|
-
role="menuitem"
|
|
811
|
-
type="button"
|
|
812
|
-
@click=${(x) => {
|
|
813
|
-
x.confirmingCompact = false;
|
|
814
|
-
x.confirmingClear = false;
|
|
815
|
-
}}
|
|
816
|
-
>
|
|
817
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="xmark"></${iconTag}></span>
|
|
818
|
-
<span class="menu-item-body"><span class="menu-item-name">Cancel</span></span>
|
|
819
|
-
</button>
|
|
820
|
-
`;
|
|
821
|
-
|
|
822
|
-
const sessionMenuConfirmCompactTemplate = html<FoundationAiAssistant>`
|
|
823
|
-
<button
|
|
824
|
-
class="menu-item"
|
|
825
|
-
role="menuitem"
|
|
826
|
-
type="button"
|
|
827
|
-
@click=${(x) => {
|
|
828
|
-
void x.compactSession();
|
|
829
|
-
}}
|
|
830
|
-
>
|
|
831
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="compress"></${iconTag}></span>
|
|
832
|
-
<span class="menu-item-body">
|
|
833
|
-
<span class="menu-item-name">Compact now</span>
|
|
834
|
-
<span class="menu-item-hint">Older turns are replaced by a summary</span>
|
|
835
|
-
</span>
|
|
836
|
-
</button>
|
|
837
|
-
${sessionMenuCancelTemplate}
|
|
838
|
-
`;
|
|
839
|
-
|
|
840
|
-
const sessionMenuConfirmClearTemplate = html<FoundationAiAssistant>`
|
|
841
|
-
<button
|
|
842
|
-
class="menu-item is-danger"
|
|
843
|
-
role="menuitem"
|
|
844
|
-
type="button"
|
|
845
|
-
@click=${(x) => {
|
|
846
|
-
void x.clearSession();
|
|
847
|
-
}}
|
|
848
|
-
>
|
|
849
|
-
<span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
|
|
850
|
-
<span class="menu-item-body">
|
|
851
|
-
<span class="menu-item-name">Clear everything</span>
|
|
852
|
-
<span class="menu-item-hint">This conversation is deleted permanently</span>
|
|
853
|
-
</span>
|
|
854
|
-
</button>
|
|
855
|
-
${sessionMenuCancelTemplate}
|
|
856
|
-
`;
|
|
857
|
-
|
|
858
|
-
const sessionMenuPanelTemplate = html<FoundationAiAssistant>`
|
|
859
|
-
<div
|
|
860
|
-
class="composer-menu session-menu-panel"
|
|
861
|
-
part="session-menu-panel"
|
|
862
|
-
role="menu"
|
|
863
|
-
aria-label="Session options"
|
|
864
|
-
>
|
|
865
|
-
${when((x) => !x.confirmingClear && !x.confirmingCompact, sessionMenuIdleTemplate)}${when(
|
|
866
|
-
(x) => x.confirmingCompact,
|
|
867
|
-
sessionMenuConfirmCompactTemplate,
|
|
868
|
-
)}${when((x) => x.confirmingClear, sessionMenuConfirmClearTemplate)}
|
|
869
|
-
</div>
|
|
870
|
-
`;
|
|
871
|
-
|
|
872
|
-
// ── Root template ───────────────────────────────────────────────────────────
|
|
873
|
-
|
|
874
|
-
return html<FoundationAiAssistant>`
|
|
875
|
-
<div class="chat-wrapper" part="chat-wrapper">
|
|
876
|
-
${when(
|
|
877
|
-
(x) => !!x.chatConfig.ui?.acceptedFiles,
|
|
878
|
-
html<FoundationAiAssistant>`
|
|
879
|
-
<input
|
|
880
|
-
class="file-input"
|
|
881
|
-
type="file"
|
|
882
|
-
multiple
|
|
883
|
-
accept=${(x) => x.chatConfig.ui?.acceptedFiles}
|
|
884
|
-
@change=${(x, c) => x.handleFileSelect(c.event)}
|
|
885
|
-
/>
|
|
886
|
-
`,
|
|
887
|
-
)}
|
|
888
|
-
${when(
|
|
889
|
-
(x) => x.settingsCogVisible,
|
|
890
|
-
html<FoundationAiAssistant>`
|
|
891
|
-
<div
|
|
892
|
-
class="chat-header ${(x) => (x.popoutMode !== 'collapse' ? 'draggable' : '')}"
|
|
893
|
-
part="chat-header"
|
|
894
|
-
@mousedown="${(x, c) => x.onChatHeaderMouseDown(c.event as MouseEvent)}"
|
|
895
|
-
>
|
|
896
|
-
${when(
|
|
897
|
-
(x) => !!x.headerTitle,
|
|
898
|
-
html<FoundationAiAssistant>`
|
|
899
|
-
<span class="chat-title" part="chat-title">${(x) => x.headerTitle}</span>
|
|
900
|
-
`,
|
|
901
|
-
)}
|
|
902
|
-
<div class="header-actions" part="header-actions">
|
|
903
|
-
<slot name="header-actions-extra"></slot>
|
|
904
|
-
${when(
|
|
905
|
-
(x) => !!x.popoutMode,
|
|
906
|
-
html<FoundationAiAssistant>`
|
|
907
|
-
<${buttonTag}
|
|
908
|
-
class="popout-button"
|
|
909
|
-
part="popout-button"
|
|
910
|
-
appearance="stealth"
|
|
911
|
-
title="${(x) => (x.popoutMode === 'expand' ? 'Expand to panel' : 'Pop back to bubble')}"
|
|
912
|
-
?disabled=${(x) => x.hasActivePendingInteraction}
|
|
913
|
-
@click=${(x) => x.handlePopout()}
|
|
914
|
-
><${iconTag} name="${(x) => (x.popoutMode === 'expand' ? 'external-link-alt' : 'compress-alt')}"></${iconTag}></${buttonTag}>
|
|
915
|
-
`,
|
|
916
|
-
)}
|
|
917
|
-
<${buttonTag}
|
|
918
|
-
class="settings-button"
|
|
919
|
-
part="settings-button"
|
|
920
|
-
appearance="stealth"
|
|
921
|
-
title="Settings"
|
|
922
|
-
aria-haspopup="dialog"
|
|
923
|
-
@click=${(x) => x.openSettingsModal()}
|
|
924
|
-
><${iconTag} name="cog"></${iconTag}></${buttonTag}>
|
|
925
|
-
</div>
|
|
926
|
-
</div>
|
|
927
|
-
`,
|
|
928
|
-
)}
|
|
929
|
-
|
|
930
|
-
<slot name="below-header"></slot>
|
|
931
|
-
|
|
932
|
-
<div class="splash-wrapper" part="splash">
|
|
933
|
-
<slot name="splash"></slot>
|
|
934
|
-
</div>
|
|
935
|
-
|
|
936
|
-
<div class="messages" part="messages" ${ref('messagesEl')}>
|
|
937
|
-
<div class="messages-content" part="messages-content">
|
|
938
|
-
${when((x) => x.compacting, compactingTemplate)}
|
|
939
|
-
${when((x) => x.restoring, restoringTemplate)}
|
|
940
|
-
${repeat(
|
|
941
|
-
(x) => (x.compacting || x.restoring ? [] : x.visibleMessages),
|
|
942
|
-
messageRowTemplate,
|
|
943
|
-
)}
|
|
944
|
-
${
|
|
945
|
-
/* GENC-1429: recycle:false so each pending interaction gets a FRESH wrapper. With
|
|
946
|
-
recycling, the trailing slot's wrapper is reused across interactions (e.g. a tool that
|
|
947
|
-
shows the code-diff approval, then the preview-status card): the reused wrapper still
|
|
948
|
-
carries the prior interaction's truthy `resolved` (bound last, after componentName/
|
|
949
|
-
interactionId trigger the re-render), so the completion listener's `if (this.resolved)
|
|
950
|
-
return` guard swallows the next auto-resolve widget's completion and the driver hangs
|
|
951
|
-
awaiting it. A fresh wrapper starts with resolved=undefined, so the guard is correct. */ ''
|
|
952
|
-
}
|
|
953
|
-
${repeat((x) => x.activeInteractionRow, messageRowTemplate, { recycle: false })}
|
|
954
|
-
${when(
|
|
955
|
-
(x) => x.liveSubAgentTrace.length > 0 && x.showToolCalls,
|
|
956
|
-
liveSubAgentTraceTemplate,
|
|
957
|
-
)}
|
|
958
|
-
${when(
|
|
959
|
-
(x) =>
|
|
960
|
-
x.showLoadingIndicator &&
|
|
961
|
-
(x.chatConfig.ui?.animations == null ||
|
|
962
|
-
LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s))),
|
|
963
|
-
html<FoundationAiAssistant>`
|
|
964
|
-
<div class="message-row ai" part="thinking">
|
|
965
|
-
<div class="avatar">
|
|
966
|
-
${(x) =>
|
|
967
|
-
x.imageSrc
|
|
968
|
-
? html<FoundationAiAssistant>`
|
|
969
|
-
<img src="${() => x.imageSrc}" alt="Assistant" class="avatar-img" />
|
|
970
|
-
`
|
|
971
|
-
: html<FoundationAiAssistant>`
|
|
972
|
-
<span
|
|
973
|
-
class="avatar-icon"
|
|
974
|
-
:innerHTML="${() => x.assistantIconSafe}"
|
|
975
|
-
></span>
|
|
976
|
-
`}
|
|
977
|
-
</div>
|
|
978
|
-
${(x) => selectThinkingTemplate(x)}
|
|
979
|
-
</div>
|
|
980
|
-
`,
|
|
981
|
-
)}
|
|
982
|
-
</div>
|
|
983
|
-
</div>
|
|
984
|
-
|
|
985
|
-
${when(
|
|
986
|
-
(x) =>
|
|
987
|
-
!x.composerHiddenByConfig && (x.attachments.length > 0 || x.attachmentErrors.length > 0),
|
|
988
|
-
html<FoundationAiAssistant>`
|
|
989
|
-
<div class="attachment-chips" part="attachment-chips">
|
|
990
|
-
${repeat(
|
|
991
|
-
(x) => x.attachments,
|
|
992
|
-
html<ChatAttachment, FoundationAiAssistant>`
|
|
993
|
-
<span class="attachment-chip" part="attachment-chip">
|
|
994
|
-
${(a) => a.name}
|
|
995
|
-
<button
|
|
996
|
-
class="chip-remove"
|
|
997
|
-
type="button"
|
|
998
|
-
@click=${(a, c) => c.parent.removeAttachment(a)}
|
|
999
|
-
>
|
|
1000
|
-
×
|
|
1001
|
-
</button>
|
|
1002
|
-
</span>
|
|
1003
|
-
`,
|
|
1004
|
-
)}
|
|
1005
|
-
${repeat(
|
|
1006
|
-
(x) => x.attachmentErrors,
|
|
1007
|
-
html<string, FoundationAiAssistant>`
|
|
1008
|
-
<span class="attachment-chip attachment-chip-error" part="attachment-chip-error">
|
|
1009
|
-
${(err) => err}
|
|
1010
|
-
<button
|
|
1011
|
-
class="chip-remove"
|
|
1012
|
-
type="button"
|
|
1013
|
-
@click=${(err, c) => c.parent.removeAttachmentError(err)}
|
|
1014
|
-
>
|
|
1015
|
-
×
|
|
1016
|
-
</button>
|
|
1017
|
-
</span>
|
|
1018
|
-
`,
|
|
1019
|
-
)}
|
|
1020
|
-
</div>
|
|
1021
|
-
`,
|
|
1022
|
-
)}
|
|
1023
|
-
${when(
|
|
1024
|
-
(x) => x.chatConfig.suggestions?.behavior !== 'never' && !x.suggestionsBlocked,
|
|
1025
|
-
html<FoundationAiAssistant>`
|
|
1026
|
-
<chat-suggestions
|
|
1027
|
-
:state="${(x) => x.suggestionsState}"
|
|
1028
|
-
@suggestion-clicked="${(x, c) =>
|
|
1029
|
-
x.handleSuggestionClick((c.event as CustomEvent).detail)}"
|
|
1030
|
-
></chat-suggestions>
|
|
1031
|
-
`,
|
|
1032
|
-
)}
|
|
1033
|
-
${
|
|
1034
|
-
/* Rendered above the composer and independently of it — a host that hides the
|
|
1035
|
-
built-in composer (`composerHiddenByConfig`) still needs to be told why its own
|
|
1036
|
-
input is being refused. */ ''
|
|
1037
|
-
}
|
|
1038
|
-
${blockedBannerTemplate}
|
|
1039
|
-
${when(
|
|
1040
|
-
(x) =>
|
|
1041
|
-
!x.composerHiddenByConfig &&
|
|
1042
|
-
!(x.busy && x.effectiveChatInputDuringExecution === 'hidden'),
|
|
1043
|
-
html<FoundationAiAssistant>`
|
|
1044
|
-
${when(
|
|
1045
|
-
(x) => x.isComposerResizable,
|
|
1046
|
-
html<FoundationAiAssistant>`
|
|
1047
|
-
<div
|
|
1048
|
-
class="composer-resize-handle"
|
|
1049
|
-
part="composer-resize-handle"
|
|
1050
|
-
role="separator"
|
|
1051
|
-
aria-orientation="horizontal"
|
|
1052
|
-
title="Drag to resize the message box"
|
|
1053
|
-
@pointerdown=${(x, c) => x.onComposerResizeStart(c.event as PointerEvent)}
|
|
1054
|
-
@pointermove=${(x, c) => x.onComposerResizeMove(c.event as PointerEvent)}
|
|
1055
|
-
@pointerup=${(x, c) => x.onComposerResizeEnd(c.event as PointerEvent)}
|
|
1056
|
-
@pointercancel=${(x, c) => x.onComposerResizeEnd(c.event as PointerEvent)}
|
|
1057
|
-
></div>
|
|
1058
|
-
`,
|
|
1059
|
-
)}
|
|
1060
|
-
<div class="composer" part="composer">
|
|
1061
|
-
<${textareaTag}
|
|
1062
|
-
${ref('chatInputEl')}
|
|
1063
|
-
class="chat-input"
|
|
1064
|
-
part="input"
|
|
1065
|
-
style=${(x) => x.composerHeightStyle}
|
|
1066
|
-
placeholder=${(x) => x.effectivePlaceholder}
|
|
1067
|
-
:value=${(x) => x.inputValue}
|
|
1068
|
-
?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
|
|
1069
|
-
aria-disabled=${(x) => (x.blocked ? 'true' : null)}
|
|
1070
|
-
aria-label=${(x) => (x.blocked ? x.effectiveBlockedReason : null)}
|
|
1071
|
-
aria-describedby=${composerDescribedBy}
|
|
1072
|
-
@input=${(x, c) => (x.inputValue = (c.event.target as any).value)}
|
|
1073
|
-
@keydown=${(x, c) => {
|
|
1074
|
-
if (
|
|
1075
|
-
(c.event as KeyboardEvent).key === 'Enter' &&
|
|
1076
|
-
!(c.event as KeyboardEvent).shiftKey
|
|
1077
|
-
) {
|
|
1078
|
-
c.event.preventDefault();
|
|
1079
|
-
x.handleSendClick();
|
|
1080
|
-
return false;
|
|
1081
|
-
}
|
|
1082
|
-
return true;
|
|
1083
|
-
}}
|
|
1084
|
-
></${textareaTag}>
|
|
1085
|
-
<div class="composer-bar" part="composer-bar">
|
|
1086
|
-
${composerBarStartTemplate}
|
|
1087
|
-
${
|
|
1088
|
-
/* Stop and Send are mutually exclusive and share this slot — see
|
|
1089
|
-
`stopButtonVisible`. The only state with neither is a host that
|
|
1090
|
-
suppressed Stop, which keeps a disabled Send instead. */ ''
|
|
1091
|
-
}
|
|
1092
|
-
<div class="composer-bar-end" part="composer-bar-end">
|
|
1093
|
-
${when((x) => x.stopButtonVisible, stopButtonTemplate)}${when(
|
|
1094
|
-
(x) => x.sendButtonVisible,
|
|
1095
|
-
sendButtonTemplate,
|
|
1096
|
-
)}
|
|
1097
|
-
</div>
|
|
1098
|
-
</div>
|
|
1099
|
-
${when(
|
|
1100
|
-
(x) => x.agentPickerEnabled && x.agentPickerOpen,
|
|
1101
|
-
agentPickerPanelTemplate,
|
|
1102
|
-
)}${when((x) => x.sessionMenuOpen && !x.busy, sessionMenuPanelTemplate)}
|
|
1103
|
-
</div>
|
|
1104
|
-
</div>
|
|
1105
|
-
`,
|
|
1106
|
-
)}
|
|
1107
|
-
<ai-halo-overlay
|
|
1108
|
-
part="halo-overlay"
|
|
1109
|
-
?active=${(x) => x.showHalo !== 'no' && x.enabledAnimations.includes('halo')}
|
|
1110
|
-
:speed="${(x) =>
|
|
1111
|
-
x.showHalo === 'orchestrating' ? HALO_SPEED_ORCHESTRATING : HALO_SPEED_DEFAULT}"
|
|
1112
|
-
direction="${(x) => (x.showHalo === 'orchestrating' ? 'ccw' : 'cw')}"
|
|
1113
|
-
:borderSize="${(x) => (x.showHalo === 'orchestrating' ? 2 : HALO_BORDER_SIZE_DEFAULT)}"
|
|
1114
|
-
></ai-halo-overlay>
|
|
1115
|
-
</div>
|
|
1116
|
-
${SettingsModalTemplate(designSystemPrefix)}
|
|
1117
|
-
${persistenceBrokenModalTemplate(designSystemPrefix)}
|
|
1118
|
-
`;
|
|
1119
|
-
};
|