@genesislcap/ai-assistant 14.451.1-alpha-3c3e1d3.0 → 14.451.2

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.
Files changed (37) hide show
  1. package/dist/ai-assistant.api.json +37 -2
  2. package/dist/ai-assistant.d.ts +26 -7
  3. package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -1
  4. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  5. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +11 -0
  6. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  7. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +2 -0
  8. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +1 -0
  9. package/dist/dts/main/main.d.ts +14 -3
  10. package/dist/dts/main/main.d.ts.map +1 -1
  11. package/dist/dts/main/main.template.d.ts.map +1 -1
  12. package/dist/dts/state/debug-event-log.d.ts +34 -6
  13. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  14. package/dist/dts/utils/message-partition.d.ts +37 -0
  15. package/dist/dts/utils/message-partition.d.ts.map +1 -0
  16. package/dist/dts/utils/message-partition.test.d.ts +2 -0
  17. package/dist/dts/utils/message-partition.test.d.ts.map +1 -0
  18. package/dist/esm/components/chat-driver/chat-driver.js +66 -18
  19. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +16 -3
  20. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +77 -0
  21. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +1 -1
  22. package/dist/esm/main/main.js +23 -23
  23. package/dist/esm/main/main.template.js +1 -0
  24. package/dist/esm/state/debug-event-log.js +45 -10
  25. package/dist/esm/utils/message-partition.js +49 -0
  26. package/dist/esm/utils/message-partition.test.js +69 -0
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +16 -16
  29. package/src/components/chat-driver/chat-driver.ts +67 -18
  30. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +105 -0
  31. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +17 -3
  32. package/src/components/orchestrating-driver/orchestrating-driver.ts +1 -1
  33. package/src/main/main.template.ts +1 -0
  34. package/src/main/main.ts +24 -22
  35. package/src/state/debug-event-log.ts +74 -10
  36. package/src/utils/message-partition.test.ts +101 -0
  37. package/src/utils/message-partition.ts +66 -0
@@ -32,6 +32,19 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
32
32
  componentNameChanged() {
33
33
  this.renderComponent();
34
34
  }
35
+ /**
36
+ * Re-render the hosted widget when the interaction identity changes. This
37
+ * ensures widgets don't go stale with them being rendered with `recycle: true`
38
+ * when messages are dynamically filtered, showing/hiding thinking/tool calls
39
+ *
40
+ * `interactionId` is a per-interaction UUID, so it changes exactly when the
41
+ * hosted interaction does. Property bindings apply in template order
42
+ * (componentName, data, interactionId), so `data` is already rebound by the
43
+ * time this fires and the fresh widget is built with the correct data.
44
+ */
45
+ interactionIdChanged() {
46
+ this.renderComponent();
47
+ }
35
48
  resolvedChanged() {
36
49
  var _a;
37
50
  const element = (_a = this.container) === null || _a === void 0 ? void 0 : _a.firstElementChild;
@@ -74,9 +87,9 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
74
87
  this._resizeObserver = undefined;
75
88
  this.container.replaceChildren();
76
89
  if (!customElements.get(this.componentName)) {
77
- logger.warn(`Interactive component "\${this.componentName}" is not registered in the customElements registry.`);
90
+ logger.warn(`Interactive component "${this.componentName}" is not registered in the customElements registry.`);
78
91
  const errorDiv = document.createElement('div');
79
- errorDiv.textContent = `Error: Component "\${this.componentName}" is not available in this application.`;
92
+ errorDiv.textContent = `Error: Component "${this.componentName}" is not available in this application.`;
80
93
  this.container.appendChild(errorDiv);
81
94
  return;
82
95
  }
@@ -108,7 +121,7 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
108
121
  catch (e) {
109
122
  logger.error(`Failed to create interactive component: \${this.componentName}`, e);
110
123
  const errorDiv = document.createElement('div');
111
- errorDiv.textContent = `Error: Could not load component \${this.componentName}`;
124
+ errorDiv.textContent = `Error: Could not load component ${this.componentName}`;
112
125
  this.container.appendChild(errorDiv);
113
126
  }
114
127
  }
@@ -0,0 +1,77 @@
1
+ import { __awaiter } from "tslib";
2
+ import { assert, createComponentSuite } from '@genesislcap/foundation-testing';
3
+ import { DOM } from '@genesislcap/web-core';
4
+ import { AiChatInteractionWrapper } from './chat-interaction-wrapper';
5
+ // Hold a reference so the custom-element registration isn't tree-shaken.
6
+ AiChatInteractionWrapper;
7
+ /**
8
+ * Minimal stand-in for a real interaction widget. The wrapper instantiates the
9
+ * element named by `componentName` and assigns `data`/`resolved` onto it; the
10
+ * tests read those back to verify which interaction the rendered widget belongs
11
+ * to.
12
+ */
13
+ const FAKE_WIDGET = 'test-interaction-widget';
14
+ if (!customElements.get(FAKE_WIDGET)) {
15
+ customElements.define(FAKE_WIDGET, class extends HTMLElement {
16
+ });
17
+ }
18
+ /**
19
+ * Renders a first interaction into a freshly-mounted wrapper.
20
+ *
21
+ * `componentName` is assigned last on purpose. The suite hands us an
22
+ * already-connected element, so the only render trigger left is
23
+ * `componentNameChanged`; setting it last means that render sees `data` and
24
+ * `interactionId` already in place — reproducing production, where the initial
25
+ * widget renders in `connectedCallback` with every bound property present. This
26
+ * keeps the *first* render correct on both the patched and unpatched code so
27
+ * that the id-change rebind is the only behaviour each test isolates.
28
+ */
29
+ function renderFirstInteraction(element) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ yield DOM.nextUpdate();
32
+ element.data = { label: 'interaction-a' };
33
+ element.interactionId = 'id-a';
34
+ element.componentName = FAKE_WIDGET;
35
+ yield DOM.nextUpdate();
36
+ });
37
+ }
38
+ const Suite = createComponentSuite('AiChatInteractionWrapper', 'ai-chat-interaction-wrapper');
39
+ /**
40
+ * Regression for the cog-menu toggle misalignment: FAST's `repeat` reconciles
41
+ * the message list by array index (it has no key function), so toggling "Tool
42
+ * calls"/"Thinking" re-filters the visible messages and rebinds an existing
43
+ * row's wrapper to a *different* interaction. When both interactions use the
44
+ * same component, `componentNameChanged` doesn't fire — so the wrapper must
45
+ * re-render on `interactionId` change, or the previous interaction's widget
46
+ * stays mounted under the new message.
47
+ */
48
+ Suite('re-renders the hosted widget when interactionId changes with the same componentName', (_a) => __awaiter(void 0, [_a], void 0, function* ({ element }) {
49
+ yield renderFirstInteraction(element);
50
+ const firstWidget = element.container.firstElementChild;
51
+ assert.ok(firstWidget, 'a widget is rendered for the first interaction');
52
+ assert.equal(firstWidget.data, { label: 'interaction-a' });
53
+ // Rebind the reused wrapper to a different interaction that happens to use
54
+ // the same component — `data` then `interactionId`, mirroring the template's
55
+ // property-binding order. `componentName` is unchanged, so the only thing
56
+ // that can drive a re-render is the interactionId hook.
57
+ element.data = { label: 'interaction-b' };
58
+ element.interactionId = 'id-b';
59
+ yield DOM.nextUpdate();
60
+ const secondWidget = element.container.firstElementChild;
61
+ assert.is.not(secondWidget, firstWidget, 'the stale widget is replaced, not left mounted');
62
+ assert.equal(secondWidget.data, { label: 'interaction-b' }, 'the rendered widget reflects the new interaction, not the stale one');
63
+ }));
64
+ /**
65
+ * Resolving an interaction in place (the user answers it) must forward the
66
+ * result without tearing down and rebuilding the widget — only a genuine change
67
+ * of interaction identity should rebuild.
68
+ */
69
+ Suite('forwards resolved in place without rebuilding the widget', (_a) => __awaiter(void 0, [_a], void 0, function* ({ element }) {
70
+ yield renderFirstInteraction(element);
71
+ const widget = element.container.firstElementChild;
72
+ element.resolved = { status: 'approved' };
73
+ yield DOM.nextUpdate();
74
+ assert.is(element.container.firstElementChild, widget, 'resolving in place must not rebuild the widget');
75
+ assert.equal(widget.resolved, { status: 'approved' }, 'resolved is forwarded to the widget');
76
+ }));
77
+ Suite.run();
@@ -222,7 +222,7 @@ export class OrchestratingDriver extends EventTarget {
222
222
  }
223
223
  handoffs += 1;
224
224
  if (handoffs > this.maxHandoffs) {
225
- this.appendInlineMessage(`I wasn't able to fully complete your request — the task required more hand-offs between specialists than allowed (max: ${this.maxHandoffs}). Please try breaking your request into smaller steps.`);
225
+ this.appendInlineMessage(`I wasn't able to fully complete your request — the task required more hand-offs between specialists than allowed. Please try breaking your request into smaller steps.`);
226
226
  break;
227
227
  }
228
228
  handoffSummary = result.summary;
@@ -41,6 +41,7 @@ import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } fr
41
41
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
42
42
  import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
43
43
  import { logger } from '../utils/logger';
44
+ import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
44
45
  import { sumCosts } from '../utils/sum-costs';
45
46
  import { expandToolTree } from '../utils/tool-fold';
46
47
  import { styles } from './main.styles';
@@ -504,9 +505,13 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
504
505
  return [];
505
506
  }
506
507
  /**
507
- * Messages filtered by the current toggle state.
508
- * Tool-related messages (those with toolCalls or toolResult) are hidden when
509
- * `showToolCalls` is false.
508
+ * Messages rendered in the scrolling list, filtered by the current toggle
509
+ * state. Tool-related messages (those with toolCalls or toolResult) are hidden
510
+ * when `showToolCalls` is false.
511
+ *
512
+ * Excludes the trailing interaction (see `activeInteractionRow`), which is
513
+ * rendered in a separate pinned slot so toggling the message filters never
514
+ * re-creates a live interaction widget.
510
515
  *
511
516
  * Marked `@volatile` because the filter branches conditionally access
512
517
  * `showToolCalls`, `showThinkingSteps`, and `showAgentSwitchIndicator`
@@ -519,28 +524,23 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
519
524
  const showAgentSwitchIndicator = ((_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showAgentSwitchIndicator) != null
520
525
  ? this.showAgentSwitchIndicator
521
526
  : this.showToolCalls;
522
- return this.messages.filter((m) => {
523
- var _a, _b, _c;
524
- // Agent switch indicators are shown when the toggle is on (or showToolCalls implies it)
525
- if (m.role === 'system-event') {
526
- return showAgentSwitchIndicator;
527
- }
528
- // Never show tool messages to the user
529
- if (m.role === 'tool') {
530
- return false;
531
- }
532
- // Filter thinking messages based on toggle
533
- if (m.thinking && !this.showThinkingSteps) {
534
- return false;
535
- }
536
- // Filter out empty assistant messages that might have slipped through
537
- if (m.role === 'assistant' && !((_a = m.content) === null || _a === void 0 ? void 0 : _a.trim()) && !((_b = m.toolCalls) === null || _b === void 0 ? void 0 : _b.length) && !m.interaction) {
538
- return false;
539
- }
540
- const isToolRelated = !!(((_c = m.toolCalls) === null || _c === void 0 ? void 0 : _c.length) || m.toolResult);
541
- return !isToolRelated || this.showToolCalls;
527
+ const { messages } = this;
528
+ const listed = trailingInteractionRow(messages).length ? messages.slice(0, -1) : messages;
529
+ return filterVisibleMessages(listed, {
530
+ showToolCalls: this.showToolCalls,
531
+ showThinkingSteps: this.showThinkingSteps,
532
+ showAgentSwitchIndicator,
542
533
  });
543
534
  }
535
+ /**
536
+ * The trailing interaction, rendered in a pinned slot below the scrolling
537
+ * list rather than inside it. Reads only `messages` (never the toggle state),
538
+ * so a cog-menu toggle leaves this binding — and the live widget's DOM —
539
+ * untouched. See `trailingInteractionRow` for the rationale.
540
+ */
541
+ get activeInteractionRow() {
542
+ return trailingInteractionRow(this.messages);
543
+ }
544
544
  agentsChanged() {
545
545
  var _a, _b, _c;
546
546
  // Guard: driver doesn't exist yet during connectedCallback — createDriver/wireDriver handle that.
@@ -432,6 +432,7 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
432
432
 
433
433
  <div class="messages" part="messages" ${ref('messagesEl')}>
434
434
  ${repeat((x) => x.visibleMessages, messageRowTemplate)}
435
+ ${repeat((x) => x.activeInteractionRow, messageRowTemplate)}
435
436
  ${when((x) => x.liveSubAgentTrace.length > 0 && x.showToolCalls, liveSubAgentTraceTemplate)}
436
437
  ${when((x) => {
437
438
  var _a;
@@ -16,8 +16,10 @@
16
16
  * there's no reason to pay immutable-reducer cost or flood the Redux DevTools
17
17
  * action log with debug noise.
18
18
  *
19
- * Surfaced under `getDebugLog().meta.events`. Ring-buffered so a long-lived
20
- * session can't grow the timeline unbounded.
19
+ * Surfaced in `getDebugLog().timeline`, merged chronologically with messages and
20
+ * turn snapshots. Ring-buffered the oldest non-`high` events are evicted first
21
+ * while `high`-importance failure events are retained — so a long-lived session
22
+ * stays bounded in normal use without ever dropping a failure signal.
21
23
  *
22
24
  * @internal
23
25
  */
@@ -45,6 +47,7 @@ export const META_EVENT_IMPORTANCE = {
45
47
  'state.changed': 'normal',
46
48
  'turn.start': 'normal',
47
49
  'turn.end': 'normal',
50
+ 'turn.retry': 'normal',
48
51
  'agent.handoff': 'normal',
49
52
  'agent.pinned': 'normal',
50
53
  'agent.unpinned': 'normal',
@@ -57,13 +60,20 @@ export const META_EVENT_IMPORTANCE = {
57
60
  'panel.toggled': 'low',
58
61
  'attachment.added': 'low',
59
62
  };
60
- /** Default ring-buffer cap. ~5× the turn-snapshot cap — entries are cheap. */
61
- const DEFAULT_MAX_META_EVENTS = 200;
63
+ /**
64
+ * Soft ring-buffer cap. Once exceeded, the oldest *non-`high`* event is evicted;
65
+ * `high`-importance events (failures and hard limits) are never dropped — they're
66
+ * the whole reason the log exists. So a buffer dominated by `high` events is
67
+ * allowed to float above this cap rather than lose a failure signal; in normal
68
+ * use the frequent `low`/`normal` events keep it near the cap. Entries are cheap.
69
+ */
70
+ const DEFAULT_MAX_META_EVENTS = 400;
62
71
  const registry = new Map();
63
72
  /**
64
- * Append a meta event to the timeline for `key`. Evicts the oldest entry once
65
- * the buffer exceeds {@link DEFAULT_MAX_META_EVENTS}. An empty `key` is bucketed
66
- * under `''`, matching how drivers/stores handle an absent session identity, so
73
+ * Append a meta event to the timeline for `key`. Once the buffer exceeds
74
+ * {@link DEFAULT_MAX_META_EVENTS}, evicts the oldest *non-`high`* event;
75
+ * `high`-importance events are never evicted. An empty `key` is bucketed under
76
+ * `''`, matching how drivers/stores handle an absent session identity, so
67
77
  * callers never need to guard.
68
78
  */
69
79
  export function recordMetaEvent(key, type, detail) {
@@ -81,9 +91,34 @@ export function recordMetaEvent(key, type, detail) {
81
91
  });
82
92
  buffer.next += 1;
83
93
  if (buffer.events.length > DEFAULT_MAX_META_EVENTS) {
84
- buffer.events.shift();
94
+ // Drop the oldest event that isn't `high` importance. `high` events
95
+ // (failures/limits) are retained even past the cap — losing a failure
96
+ // signal is worse than briefly exceeding the buffer size. If every retained
97
+ // event is `high` (all-failures session), nothing is evicted and the buffer
98
+ // grows; in practice frequent `low`/`normal` events keep it bounded.
99
+ const evictAt = buffer.events.findIndex((e) => e.importance !== 'high');
100
+ if (evictAt !== -1) {
101
+ buffer.events.splice(evictAt, 1);
102
+ }
85
103
  }
86
104
  }
105
+ /**
106
+ * Record a turn-ending failure (`turn.error`, importance `high`). The `reason`
107
+ * is typed so the high-importance triage surface stays consistent; pass any
108
+ * diagnostic specifics (attempt counts, offending tool names, etc.) in `detail`.
109
+ */
110
+ export function recordTurnError(key, reason, detail) {
111
+ recordMetaEvent(key, 'turn.error', Object.assign({ reason }, detail));
112
+ }
113
+ /**
114
+ * Record a recoverable retry *within* a turn (`turn.retry`, importance `normal`)
115
+ * — e.g. one malformed/empty attempt that will be retried, as distinct from the
116
+ * final attempt that bails out as a `turn.error`. Include `attempt`/`maxAttempts`
117
+ * in `detail` so the timeline shows which try this was.
118
+ */
119
+ export function recordTurnRetry(key, reason, detail) {
120
+ recordMetaEvent(key, 'turn.retry', Object.assign({ reason }, detail));
121
+ }
87
122
  /** Returns the meta-event timeline for `key`, or an empty array if none recorded. */
88
123
  export function getMetaEvents(key) {
89
124
  var _a, _b;
@@ -102,8 +137,8 @@ export const DEBUG_LOG_README = [
102
137
  "kind:'turn' — one LLM call. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
103
138
  "kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
104
139
  "kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
105
- "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
106
- 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.error (a turn failed or hit a guardrail — see detail.reason), tool.failed (a tool threw), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
140
+ "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
141
+ 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations, plus reason-specific diagnostics: attempts, finishMessage, unknownTools + availableTools, iterations + limit, or name + message for exceptions), tool.failed (a tool threw), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
107
142
  "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
108
143
  'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',
109
144
  ];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The trailing interaction to render in a pinned slot, separate from the
3
+ * filtered list. Empty unless the most recent message carries an interaction.
4
+ *
5
+ * Pinning the trailing interaction keeps its widget out of the list's
6
+ * reconciliation: FAST's `repeat` has no key function and reconciles by array
7
+ * index, so re-filtering the list (a cog-menu toggle) would otherwise rebind an
8
+ * existing row to a different message and re-create the live widget — discarding
9
+ * any in-progress input. The slot's binding reads only `messages` and returns
10
+ * the same message object across toggles, so the widget's DOM is preserved.
11
+ *
12
+ * It is the *trailing* message (resolved or not), not "unresolved only", so that
13
+ * answering an interaction leaves it in the slot — `resolved` is forwarded in
14
+ * place with no re-render. It rejoins the list naturally once a newer message
15
+ * arrives and it is no longer last.
16
+ */
17
+ export function trailingInteractionRow(messages) {
18
+ const last = messages[messages.length - 1];
19
+ return (last === null || last === void 0 ? void 0 : last.interaction) ? [last] : [];
20
+ }
21
+ /**
22
+ * Filters messages for the scrolling list per the current cog-menu toggles.
23
+ *
24
+ * The trailing interaction (see `trailingInteractionRow`) must be removed
25
+ * before calling, as it is rendered separately.
26
+ */
27
+ export function filterVisibleMessages(messages, toggles) {
28
+ return messages.filter((m) => {
29
+ var _a, _b, _c;
30
+ // Agent switch indicators show when their toggle is on (or showToolCalls implies it).
31
+ if (m.role === 'system-event') {
32
+ return toggles.showAgentSwitchIndicator;
33
+ }
34
+ // Tool result messages are never shown to the user.
35
+ if (m.role === 'tool') {
36
+ return false;
37
+ }
38
+ // Thinking messages follow their own toggle.
39
+ if (m.thinking && !toggles.showThinkingSteps) {
40
+ return false;
41
+ }
42
+ // Drop empty assistant messages that slipped through.
43
+ if (m.role === 'assistant' && !((_a = m.content) === null || _a === void 0 ? void 0 : _a.trim()) && !((_b = m.toolCalls) === null || _b === void 0 ? void 0 : _b.length) && !m.interaction) {
44
+ return false;
45
+ }
46
+ const isToolRelated = !!(((_c = m.toolCalls) === null || _c === void 0 ? void 0 : _c.length) || m.toolResult);
47
+ return !isToolRelated || toggles.showToolCalls;
48
+ });
49
+ }
@@ -0,0 +1,69 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { filterVisibleMessages, trailingInteractionRow, } from './message-partition';
3
+ const msg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
4
+ const interaction = (interactionId, resolved = false) => (Object.assign({ interactionId, componentName: 'test-widget', data: {} }, (resolved ? { resolved: { status: 'approved' } } : {})));
5
+ const ALL_ON = {
6
+ showToolCalls: true,
7
+ showThinkingSteps: true,
8
+ showAgentSwitchIndicator: true,
9
+ };
10
+ const ALL_OFF = {
11
+ showToolCalls: false,
12
+ showThinkingSteps: false,
13
+ showAgentSwitchIndicator: false,
14
+ };
15
+ const trailing = createLogicSuite('trailingInteractionRow');
16
+ trailing('is empty when there are no messages', () => {
17
+ assert.equal(trailingInteractionRow([]), []);
18
+ });
19
+ trailing('is empty when the last message has no interaction', () => {
20
+ assert.equal(trailingInteractionRow([msg({ content: 'hi' })]), []);
21
+ });
22
+ trailing('returns the last message when it carries an interaction (pending)', () => {
23
+ const pending = msg({ interaction: interaction('id-a') });
24
+ assert.equal(trailingInteractionRow([msg({ content: 'q' }), pending]), [pending]);
25
+ });
26
+ trailing('still returns the trailing interaction once it is resolved', () => {
27
+ // Keeps it in the pinned slot so resolving forwards in place without a rebuild.
28
+ const resolved = msg({ interaction: interaction('id-a', true) });
29
+ assert.equal(trailingInteractionRow([resolved]), [resolved]);
30
+ });
31
+ trailing('returns nothing once a newer non-interaction message arrives', () => {
32
+ // The interaction is no longer last, so it rejoins the scrolling list.
33
+ const earlier = msg({ interaction: interaction('id-a', true) });
34
+ assert.equal(trailingInteractionRow([earlier, msg({ content: 'next' })]), []);
35
+ });
36
+ trailing('returns the same message reference (lets FAST short-circuit rebind)', () => {
37
+ const pending = msg({ interaction: interaction('id-a') });
38
+ assert.is(trailingInteractionRow([pending])[0], pending);
39
+ });
40
+ const filter = createLogicSuite('filterVisibleMessages');
41
+ filter('hides tool result messages regardless of toggles', () => {
42
+ const msgs = [msg({ role: 'tool', toolResult: { toolCallId: 't1', content: 'r' } })];
43
+ assert.equal(filterVisibleMessages(msgs, ALL_ON), []);
44
+ });
45
+ filter('hides tool-call messages when showToolCalls is off, shows them when on', () => {
46
+ const toolCall = msg({ toolCalls: [{ id: 't1', name: 'do', args: {} }] });
47
+ assert.equal(filterVisibleMessages([toolCall], ALL_OFF), []);
48
+ assert.equal(filterVisibleMessages([toolCall], ALL_ON), [toolCall]);
49
+ });
50
+ filter('hides thinking messages when showThinkingSteps is off', () => {
51
+ const thinking = msg({ thinking: true, content: 'hmm' });
52
+ assert.equal(filterVisibleMessages([thinking], ALL_OFF), []);
53
+ assert.equal(filterVisibleMessages([thinking], ALL_ON), [thinking]);
54
+ });
55
+ filter('hides system-event rows unless the agent-switch toggle is on', () => {
56
+ const event = msg({ role: 'system-event', content: 'switched' });
57
+ assert.equal(filterVisibleMessages([event], ALL_OFF), []);
58
+ assert.equal(filterVisibleMessages([event], ALL_ON), [event]);
59
+ });
60
+ filter('drops empty assistant messages', () => {
61
+ assert.equal(filterVisibleMessages([msg({ content: ' ' })], ALL_ON), []);
62
+ });
63
+ filter('keeps user and non-empty assistant messages with everything off', () => {
64
+ const user = msg({ role: 'user', content: 'hello' });
65
+ const reply = msg({ content: 'hi there' });
66
+ assert.equal(filterVisibleMessages([user, reply], ALL_OFF), [user, reply]);
67
+ });
68
+ trailing.run();
69
+ filter.run();
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
1
+ {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "14.451.1-alpha-3c3e1d3.0",
4
+ "version": "14.451.2",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -64,24 +64,24 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@genesislcap/foundation-testing": "14.451.1-alpha-3c3e1d3.0",
68
- "@genesislcap/genx": "14.451.1-alpha-3c3e1d3.0",
69
- "@genesislcap/rollup-builder": "14.451.1-alpha-3c3e1d3.0",
70
- "@genesislcap/ts-builder": "14.451.1-alpha-3c3e1d3.0",
71
- "@genesislcap/uvu-playwright-builder": "14.451.1-alpha-3c3e1d3.0",
72
- "@genesislcap/vite-builder": "14.451.1-alpha-3c3e1d3.0",
73
- "@genesislcap/webpack-builder": "14.451.1-alpha-3c3e1d3.0",
67
+ "@genesislcap/foundation-testing": "14.451.2",
68
+ "@genesislcap/genx": "14.451.2",
69
+ "@genesislcap/rollup-builder": "14.451.2",
70
+ "@genesislcap/ts-builder": "14.451.2",
71
+ "@genesislcap/uvu-playwright-builder": "14.451.2",
72
+ "@genesislcap/vite-builder": "14.451.2",
73
+ "@genesislcap/webpack-builder": "14.451.2",
74
74
  "@types/dompurify": "^3.0.5",
75
75
  "@types/marked": "^5.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.451.1-alpha-3c3e1d3.0",
79
- "@genesislcap/foundation-logger": "14.451.1-alpha-3c3e1d3.0",
80
- "@genesislcap/foundation-redux": "14.451.1-alpha-3c3e1d3.0",
81
- "@genesislcap/foundation-ui": "14.451.1-alpha-3c3e1d3.0",
82
- "@genesislcap/foundation-utils": "14.451.1-alpha-3c3e1d3.0",
83
- "@genesislcap/rapid-design-system": "14.451.1-alpha-3c3e1d3.0",
84
- "@genesislcap/web-core": "14.451.1-alpha-3c3e1d3.0",
78
+ "@genesislcap/foundation-ai": "14.451.2",
79
+ "@genesislcap/foundation-logger": "14.451.2",
80
+ "@genesislcap/foundation-redux": "14.451.2",
81
+ "@genesislcap/foundation-ui": "14.451.2",
82
+ "@genesislcap/foundation-utils": "14.451.2",
83
+ "@genesislcap/rapid-design-system": "14.451.2",
84
+ "@genesislcap/web-core": "14.451.2",
85
85
  "dompurify": "^3.3.1",
86
86
  "marked": "^17.0.3"
87
87
  },
@@ -93,5 +93,5 @@
93
93
  "publishConfig": {
94
94
  "access": "public"
95
95
  },
96
- "gitHead": "71f52da6801da778a24804715814985bf09b074c"
96
+ "gitHead": "44a0946f3ff1f184fd6fd8f877108b0dbaf2d9e0"
97
97
  }