@genesislcap/ai-assistant 15.26.0 → 15.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-assistant.api.json +150 -7
- package/dist/ai-assistant.d.ts +82 -99
- package/dist/chat-driver.cjs +460 -373
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +460 -373
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +682 -202
- package/dist/dts/components/chat-driver/chat-driver.d.ts +28 -200
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +99 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts +191 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +20 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +32 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts +19 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +7 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persistence-provider.d.ts +5 -0
- package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/format-message-timestamp.d.ts +77 -0
- package/dist/dts/utils/format-message-timestamp.d.ts.map +1 -0
- package/dist/dts/utils/resolve-preference-baseline.d.ts +18 -1
- package/dist/dts/utils/resolve-preference-baseline.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +68 -404
- package/dist/esm/components/chat-driver/interaction-controller.js +235 -0
- package/dist/esm/components/chat-driver/turn-recorder.js +206 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +53 -2
- package/dist/esm/main/main.js +80 -17
- package/dist/esm/main/main.styles.js +75 -0
- package/dist/esm/main/main.template.js +43 -2
- package/dist/esm/state/ai-assistant-slice.js +5 -0
- package/dist/esm/utils/format-message-timestamp.js +126 -0
- package/dist/esm/utils/resolve-preference-baseline.js +54 -2
- package/package.json +16 -16
- package/sandbox/README.md +45 -2
- package/sandbox/sandbox.ts +95 -3
package/dist/esm/main/main.js
CHANGED
|
@@ -58,7 +58,7 @@ import { deleteBankedBaseline } from '../utils/banked-usage-baselines';
|
|
|
58
58
|
import { estimateContextTokens, isContextMeasurable, } from '../utils/context-tokens';
|
|
59
59
|
import { logger } from '../utils/logger';
|
|
60
60
|
import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
|
|
61
|
-
import { resolvePreferenceBaseline } from '../utils/resolve-preference-baseline';
|
|
61
|
+
import { DEFAULT_TIMESTAMP_DISPLAY, preferenceUnchanged, resolvePreferenceBaseline, } from '../utils/resolve-preference-baseline';
|
|
62
62
|
import { stripAgentHandlers } from '../utils/strip-agent-handlers';
|
|
63
63
|
import { emptyUsage, sumUsage, totalTokens } from '../utils/sum-usage';
|
|
64
64
|
import { AttachmentController } from './attachment-controller';
|
|
@@ -114,6 +114,14 @@ const DEFAULT_PERSISTENCE_SOURCE = 'host';
|
|
|
114
114
|
* cannot drift apart.
|
|
115
115
|
*/
|
|
116
116
|
const BLOCKED_PLACEHOLDER = 'AI usage limit reached';
|
|
117
|
+
/**
|
|
118
|
+
* How often the transcript's relative timestamps are recomputed (GENC-1257).
|
|
119
|
+
*
|
|
120
|
+
* The relative tier changes at minute granularity, so a 30s tick caps how stale a label
|
|
121
|
+
* can look at half a minute — close enough that a user watching one never sees it lag,
|
|
122
|
+
* without a per-second timer. The work per tick is one string format per visible row.
|
|
123
|
+
*/
|
|
124
|
+
const TIMESTAMP_CLOCK_INTERVAL_MS = 30000;
|
|
117
125
|
/**
|
|
118
126
|
* Budget formatting helpers, re-exported so they stay on this module's public
|
|
119
127
|
* surface (`src/index.ts` does `export * from './main/main'`). They live in
|
|
@@ -276,6 +284,16 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
276
284
|
this.persistence = {};
|
|
277
285
|
/** When set, enables Redux DevTools for this instance's session store. */
|
|
278
286
|
this.debugRedux = false;
|
|
287
|
+
/**
|
|
288
|
+
* Epoch ms the transcript's relative timestamps are measured against.
|
|
289
|
+
*
|
|
290
|
+
* A relative label is only correct at the instant it renders: FAST evaluates the
|
|
291
|
+
* binding once, and an idle conversation never re-renders, so "2 minutes ago" would
|
|
292
|
+
* still read "2 minutes ago" an hour later. Making the formatter read this observable
|
|
293
|
+
* instead of `Date.now()` gives every row one shared, invalidatable clock — a tick
|
|
294
|
+
* re-evaluates them all.
|
|
295
|
+
*/
|
|
296
|
+
this.timestampClock = Date.now();
|
|
279
297
|
// ---- Transient UI state (stays as @observable on the component) ----
|
|
280
298
|
this._suggestionsGeneration = 0;
|
|
281
299
|
/** Whether the loading spinner is currently visible. Controlled by the loading delay timer. */
|
|
@@ -622,12 +640,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
622
640
|
}
|
|
623
641
|
set showToolCalls(value) {
|
|
624
642
|
var _a;
|
|
625
|
-
|
|
626
|
-
// `@change` when its `:checked` binding is applied on render (not just on user
|
|
627
|
-
// interaction), which would otherwise spuriously mark prefs "user-changed" and
|
|
628
|
-
// save config defaults — freezing the reconciler so real stored prefs never apply
|
|
629
|
-
// on reload (GENC-1351 #1).
|
|
630
|
-
if (this.showToolCalls === value)
|
|
643
|
+
if (preferenceUnchanged(this.showToolCalls, value))
|
|
631
644
|
return;
|
|
632
645
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowToolCalls(value);
|
|
633
646
|
this.persistPreferences();
|
|
@@ -639,8 +652,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
639
652
|
}
|
|
640
653
|
set showThinkingSteps(value) {
|
|
641
654
|
var _a;
|
|
642
|
-
if (this.showThinkingSteps
|
|
643
|
-
return;
|
|
655
|
+
if (preferenceUnchanged(this.showThinkingSteps, value))
|
|
656
|
+
return;
|
|
644
657
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowThinkingSteps(value);
|
|
645
658
|
this.persistPreferences();
|
|
646
659
|
}
|
|
@@ -651,8 +664,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
651
664
|
}
|
|
652
665
|
set showNarration(value) {
|
|
653
666
|
var _a;
|
|
654
|
-
if (this.showNarration
|
|
655
|
-
return;
|
|
667
|
+
if (preferenceUnchanged(this.showNarration, value))
|
|
668
|
+
return;
|
|
656
669
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowNarration(value);
|
|
657
670
|
this.persistPreferences();
|
|
658
671
|
}
|
|
@@ -663,11 +676,55 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
663
676
|
}
|
|
664
677
|
set showAgentSwitchIndicator(value) {
|
|
665
678
|
var _a;
|
|
666
|
-
if (this.showAgentSwitchIndicator
|
|
667
|
-
return;
|
|
679
|
+
if (preferenceUnchanged(this.showAgentSwitchIndicator, value))
|
|
680
|
+
return;
|
|
668
681
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowAgentSwitchIndicator(value);
|
|
669
682
|
this.persistPreferences();
|
|
670
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* When each message shows its timestamp — `'off'`, `'hover'` (default) or `'always'`.
|
|
686
|
+
*
|
|
687
|
+
* Falls back to `DEFAULT_TIMESTAMP_DISPLAY` rather than `'off'` when there is no
|
|
688
|
+
* store yet: this preference defaults ON, so the pre-store render must match the state
|
|
689
|
+
* the baseline is about to resolve to, or timestamps would visibly pop in on connect.
|
|
690
|
+
*/
|
|
691
|
+
get timestampDisplay() {
|
|
692
|
+
var _a, _b;
|
|
693
|
+
return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.timestampDisplay) !== null && _b !== void 0 ? _b : DEFAULT_TIMESTAMP_DISPLAY;
|
|
694
|
+
}
|
|
695
|
+
set timestampDisplay(value) {
|
|
696
|
+
var _a;
|
|
697
|
+
if (preferenceUnchanged(this.timestampDisplay, value))
|
|
698
|
+
return;
|
|
699
|
+
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setTimestampDisplay(value);
|
|
700
|
+
this.persistPreferences();
|
|
701
|
+
// The mode decides whether the clock needs to run at all.
|
|
702
|
+
this.syncTimestampClock();
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Start or stop the timestamp clock to match the current mode and connection state.
|
|
706
|
+
*
|
|
707
|
+
* Idempotent, and safe to call from either lifecycle end — it is driven from
|
|
708
|
+
* `connectedCallback`, `disconnectedCallback` and the `timestampDisplay` setter, which
|
|
709
|
+
* can fire in any order. Nothing runs while the mode is `'off'` (no binding reads the
|
|
710
|
+
* clock) or while disconnected (a timer outliving its element is a leak, and a
|
|
711
|
+
* popped-out view disconnects without the session ending).
|
|
712
|
+
*/
|
|
713
|
+
syncTimestampClock() {
|
|
714
|
+
const wanted = this._sessionActive && this.timestampDisplay !== 'off';
|
|
715
|
+
if (wanted === !!this._timestampClockTimer)
|
|
716
|
+
return;
|
|
717
|
+
if (!wanted) {
|
|
718
|
+
clearInterval(this._timestampClockTimer);
|
|
719
|
+
this._timestampClockTimer = undefined;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Re-sync immediately: the element may have sat disconnected across several ticks.
|
|
723
|
+
this.timestampClock = Date.now();
|
|
724
|
+
this._timestampClockTimer = setInterval(() => {
|
|
725
|
+
this.timestampClock = Date.now();
|
|
726
|
+
}, TIMESTAMP_CLOCK_INTERVAL_MS);
|
|
727
|
+
}
|
|
671
728
|
/** Currently enabled animations. */
|
|
672
729
|
get enabledAnimations() {
|
|
673
730
|
var _a, _b;
|
|
@@ -675,10 +732,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
675
732
|
}
|
|
676
733
|
set enabledAnimations(value) {
|
|
677
734
|
var _a;
|
|
678
|
-
|
|
679
|
-
// fire the same spurious render-time `@change`.
|
|
680
|
-
const current = this.enabledAnimations;
|
|
681
|
-
if (current.length === value.length && current.every((a) => value.includes(a)))
|
|
735
|
+
if (preferenceUnchanged(this.enabledAnimations, value))
|
|
682
736
|
return;
|
|
683
737
|
(_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setEnabledAnimations(value);
|
|
684
738
|
this.persistPreferences();
|
|
@@ -2062,6 +2116,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
2062
2116
|
window.addEventListener('pagehide', this._handlePageHide);
|
|
2063
2117
|
logger.debug('FoundationAiAssistant connected');
|
|
2064
2118
|
this._sessionActive = true;
|
|
2119
|
+
// After `_sessionActive`, which `syncTimestampClock` reads as "connected".
|
|
2120
|
+
this.syncTimestampClock();
|
|
2065
2121
|
}
|
|
2066
2122
|
/**
|
|
2067
2123
|
* Switch this element to a different session **in place**, without a page reload
|
|
@@ -2151,6 +2207,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
2151
2207
|
this.costLedger.flush();
|
|
2152
2208
|
super.disconnectedCallback();
|
|
2153
2209
|
this._sessionActive = false;
|
|
2210
|
+
// After `_sessionActive` is cleared, so the sync sees "disconnected" and stops.
|
|
2211
|
+
this.syncTimestampClock();
|
|
2154
2212
|
this.stopLoadingTimer();
|
|
2155
2213
|
this.state = 'idle';
|
|
2156
2214
|
// Note: `compacting`/`restoring` are NOT cleared here. They're written through the
|
|
@@ -2740,6 +2798,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
2740
2798
|
showThinkingSteps: this.showThinkingSteps,
|
|
2741
2799
|
showNarration: this.showNarration,
|
|
2742
2800
|
showAgentSwitchIndicator: this.showAgentSwitchIndicator,
|
|
2801
|
+
timestampDisplay: this.timestampDisplay,
|
|
2743
2802
|
animations: [...this.enabledAnimations],
|
|
2744
2803
|
};
|
|
2745
2804
|
}
|
|
@@ -2800,6 +2859,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
2800
2859
|
this.showThinkingSteps = resolved.showThinkingSteps;
|
|
2801
2860
|
this.showNarration = resolved.showNarration;
|
|
2802
2861
|
this.showAgentSwitchIndicator = resolved.showAgentSwitchIndicator;
|
|
2862
|
+
this.timestampDisplay = resolved.timestampDisplay;
|
|
2803
2863
|
this.enabledAnimations = resolved.enabledAnimations;
|
|
2804
2864
|
}
|
|
2805
2865
|
finally {
|
|
@@ -3896,6 +3956,9 @@ __decorate([
|
|
|
3896
3956
|
__decorate([
|
|
3897
3957
|
volatile
|
|
3898
3958
|
], FoundationAiAssistant.prototype, "busy", null);
|
|
3959
|
+
__decorate([
|
|
3960
|
+
observable
|
|
3961
|
+
], FoundationAiAssistant.prototype, "timestampClock", void 0);
|
|
3899
3962
|
__decorate([
|
|
3900
3963
|
volatile
|
|
3901
3964
|
], FoundationAiAssistant.prototype, "effectiveChatInputDuringExecution", null);
|
|
@@ -354,11 +354,86 @@ const baseStyles = css `
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
.sender {
|
|
357
|
+
/* Flex so the timestamp sits on the sender's baseline with a real gap, rather than
|
|
358
|
+
butting against the name or needing a margin that a missing timestamp would leave
|
|
359
|
+
behind. Wraps, so a narrow bubble drops the timestamp to its own line instead of
|
|
360
|
+
stretching the bubble to fit both. */
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: baseline;
|
|
363
|
+
flex-wrap: wrap;
|
|
364
|
+
gap: 2px 8px;
|
|
357
365
|
font-weight: bold;
|
|
358
366
|
font-size: 0.9em;
|
|
359
367
|
margin-bottom: 4px;
|
|
360
368
|
}
|
|
361
369
|
|
|
370
|
+
/* Recessive against the name it sits beside: lighter, smaller and dimmed. Deliberately
|
|
371
|
+
not a colour of its own — it inherits the bubble's foreground, so it stays legible on
|
|
372
|
+
the accent-filled user bubble and the neutral assistant one without a token per
|
|
373
|
+
variant. */
|
|
374
|
+
.message-timestamp {
|
|
375
|
+
font-weight: normal;
|
|
376
|
+
font-size: 0.85em;
|
|
377
|
+
opacity: 60%;
|
|
378
|
+
white-space: nowrap;
|
|
379
|
+
|
|
380
|
+
/* Chrome, not message content. Without this, opacity 0 still leaves the text
|
|
381
|
+
selectable, so dragging across the transcript and copying picks up a timestamp on
|
|
382
|
+
every message — including the ones the reader never hovered and never saw. */
|
|
383
|
+
user-select: none;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Removed from paint and from flow, but kept in the accessibility tree — the standard
|
|
387
|
+
clip-rect idiom rather than display:none or visibility:hidden, both of which would
|
|
388
|
+
take it out of the tree and defeat the point. Carries the absolute time behind the
|
|
389
|
+
relative text; see messageTimestampTemplate. */
|
|
390
|
+
.visually-hidden {
|
|
391
|
+
position: absolute;
|
|
392
|
+
width: 1px;
|
|
393
|
+
height: 1px;
|
|
394
|
+
margin: -1px;
|
|
395
|
+
padding: 0;
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
clip-path: inset(50%);
|
|
398
|
+
white-space: nowrap;
|
|
399
|
+
border: 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* 'hover' — the default. Space is RESERVED at all times (opacity, not display) so
|
|
403
|
+
revealing the timestamp cannot reflow the bubble under the pointer; only the paint
|
|
404
|
+
changes. The sender line is rarely the widest line in a bubble, so this costs no
|
|
405
|
+
width except on very short messages. */
|
|
406
|
+
.messages-content.timestamps-hover .message-timestamp {
|
|
407
|
+
opacity: 0%;
|
|
408
|
+
transition: opacity 0.12s ease-out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.messages-content.timestamps-hover .message-row:hover .message-timestamp {
|
|
412
|
+
opacity: 60%;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Keyboard and touch reach the same reveal: neither produces a hover. The
|
|
416
|
+
focus-within rule costs nothing today (message rows are not focusable) and starts
|
|
417
|
+
working the moment anything inside one is. */
|
|
418
|
+
.messages-content.timestamps-hover .message-row:focus-within .message-timestamp {
|
|
419
|
+
opacity: 60%;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* A pointer that cannot hover (touch) would otherwise never see a timestamp, and
|
|
423
|
+
hover on touch is sticky and unpredictable where it fires at all. Show them. */
|
|
424
|
+
@media (hover: none) {
|
|
425
|
+
.messages-content.timestamps-hover .message-timestamp {
|
|
426
|
+
opacity: 60%;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* Respect a reduced-motion preference: the fade is decorative, the reveal is not. */
|
|
431
|
+
@media (prefers-reduced-motion: reduce) {
|
|
432
|
+
.messages-content.timestamps-hover .message-timestamp {
|
|
433
|
+
transition: none;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
362
437
|
.payload {
|
|
363
438
|
font-family: monospace;
|
|
364
439
|
white-space: pre-wrap;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
18
18
|
import { classNames, html, ref, repeat, when } from '@genesislcap/web-core';
|
|
19
19
|
import { SettingsModalTemplate } from '../components/settings-modal';
|
|
20
|
+
import { formatAbsoluteTimestamp, formatMessageTimestamp, messageTimestampDateTime, } from '../utils/format-message-timestamp';
|
|
20
21
|
import { compactionDividerLabel } from '../utils/history-transform';
|
|
21
22
|
import { LOADING_STYLE_ANIMATIONS } from './main.types';
|
|
22
23
|
function unknownToolPayload(tc) {
|
|
@@ -126,6 +127,35 @@ const messageParts = (m) => {
|
|
|
126
127
|
? `message message-interaction message-interaction-${interactionPresentation(m)}`
|
|
127
128
|
: `message message-${type}`;
|
|
128
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* The timestamp shown beside the sender name (GENC-1257).
|
|
132
|
+
*
|
|
133
|
+
* Two strings, not one. A screen reader announces a `<time>` element's TEXT — it has no
|
|
134
|
+
* implicit ARIA role, and the `datetime` attribute is never surfaced to the user — so the
|
|
135
|
+
* relative text is hidden from assistive tech and paired with a visually-hidden absolute
|
|
136
|
+
* string. Otherwise AT would hear "5 minutes ago" with nothing to anchor it, and in
|
|
137
|
+
* `'hover'` mode would hear it for a timestamp sighted users never asked to see.
|
|
138
|
+
* `datetime` stays because it is correct HTML and useful to tooling, not for a11y.
|
|
139
|
+
*
|
|
140
|
+
* Only the VISUAL reveal is hover-gated, in CSS — see `.message-timestamp`. `opacity: 0`
|
|
141
|
+
* leaves the element in the accessibility tree, so AT gets a time in both visible modes;
|
|
142
|
+
* `'off'` is the only mode that removes it.
|
|
143
|
+
*
|
|
144
|
+
* Reads `timestampClock` rather than `Date.now()` so every row shares one invalidatable
|
|
145
|
+
* clock and a tick re-evaluates them all; see `FoundationAiAssistant.timestampClock`.
|
|
146
|
+
*/
|
|
147
|
+
export const messageTimestampTemplate = html `
|
|
148
|
+
<time
|
|
149
|
+
class="message-timestamp"
|
|
150
|
+
part="message-timestamp"
|
|
151
|
+
datetime="${(m) => messageTimestampDateTime(m.timestamp)}"
|
|
152
|
+
>
|
|
153
|
+
<span aria-hidden="true">
|
|
154
|
+
${(m, c) => formatMessageTimestamp(m.timestamp, c.parent.timestampClock)}
|
|
155
|
+
</span>
|
|
156
|
+
<span class="visually-hidden">${(m) => formatAbsoluteTimestamp(m.timestamp)}</span>
|
|
157
|
+
</time>
|
|
158
|
+
`;
|
|
129
159
|
// ─── Sub-agent trace fragments ────────────────────────────────────────────────
|
|
130
160
|
const subAgentAssistantTemplate = html `
|
|
131
161
|
<div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
|
|
@@ -499,7 +529,8 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
499
529
|
// host sender label. Every other message keeps it.
|
|
500
530
|
(m) => !(m.interaction && interactionPresentation(m) === 'bare'), html `
|
|
501
531
|
<div class="sender">
|
|
502
|
-
|
|
532
|
+
<span class="sender-label">
|
|
533
|
+
${(m, c) => {
|
|
503
534
|
var _a;
|
|
504
535
|
return messageType(m) === 'ai-function' &&
|
|
505
536
|
m.agentName &&
|
|
@@ -507,6 +538,13 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
507
538
|
? `Tool Call · ${(_a = m.agentLabel) !== null && _a !== void 0 ? _a : m.agentName}`
|
|
508
539
|
: senderLabelFor(m);
|
|
509
540
|
}}
|
|
541
|
+
</span>
|
|
542
|
+
${when(
|
|
543
|
+
// Guarded on the stamp too: a session persisted before timestamp
|
|
544
|
+
// support restores without one, and an empty <time> would still
|
|
545
|
+
// draw the gap the flex row reserves for it.
|
|
546
|
+
(m, c) => !!m.timestamp &&
|
|
547
|
+
c.parent.timestampDisplay !== 'off', messageTimestampTemplate)}
|
|
510
548
|
</div>
|
|
511
549
|
`)}
|
|
512
550
|
<div class="content">
|
|
@@ -826,7 +864,10 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
826
864
|
</div>
|
|
827
865
|
|
|
828
866
|
<div class="messages" part="messages" ${ref('messagesEl')}>
|
|
829
|
-
<div
|
|
867
|
+
<div
|
|
868
|
+
class="messages-content ${(x) => `timestamps-${x.timestampDisplay}`}"
|
|
869
|
+
part="messages-content"
|
|
870
|
+
>
|
|
830
871
|
${when((x) => x.compacting, compactingTemplate)}
|
|
831
872
|
${when((x) => x.restoring, restoringTemplate)}
|
|
832
873
|
${repeat((x) => (x.compacting || x.restoring ? [] : x.visibleMessages), messageRowTemplate)}
|
|
@@ -13,6 +13,7 @@ export function createDefaultSessionState() {
|
|
|
13
13
|
showThinkingSteps: false,
|
|
14
14
|
showNarration: false,
|
|
15
15
|
showAgentSwitchIndicator: false,
|
|
16
|
+
timestampDisplay: 'hover',
|
|
16
17
|
enabledAnimations: [],
|
|
17
18
|
suggestionsState: { status: 'idle' },
|
|
18
19
|
contextTokens: undefined,
|
|
@@ -64,6 +65,9 @@ export const aiAssistantSlice = createSlice({
|
|
|
64
65
|
setShowAgentSwitchIndicator(state, action) {
|
|
65
66
|
state.showAgentSwitchIndicator = action.payload;
|
|
66
67
|
},
|
|
68
|
+
setTimestampDisplay(state, action) {
|
|
69
|
+
state.timestampDisplay = action.payload;
|
|
70
|
+
},
|
|
67
71
|
setEnabledAnimations(state, action) {
|
|
68
72
|
state.enabledAnimations = action.payload;
|
|
69
73
|
},
|
|
@@ -322,6 +326,7 @@ export const aiAssistantSlice = createSlice({
|
|
|
322
326
|
showThinkingSteps: state.showThinkingSteps,
|
|
323
327
|
showNarration: state.showNarration,
|
|
324
328
|
showAgentSwitchIndicator: state.showAgentSwitchIndicator,
|
|
329
|
+
timestampDisplay: state.timestampDisplay,
|
|
325
330
|
enabledAnimations: state.enabledAnimations,
|
|
326
331
|
providerStatuses: state.providerStatuses,
|
|
327
332
|
blocked: state.blocked,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place this package turns a message's `timestamp` into display text
|
|
3
|
+
* (GENC-1257).
|
|
4
|
+
*
|
|
5
|
+
* Three tiers, deliberately coarse — the transcript shows this beside the sender
|
|
6
|
+
* name, where a long string competes with the name itself:
|
|
7
|
+
*
|
|
8
|
+
* | Age | Reads |
|
|
9
|
+
* | -------------- | ------------------------------------------------------------------- |
|
|
10
|
+
* | under a minute | `Now` |
|
|
11
|
+
* | under an hour | `12 minutes ago` |
|
|
12
|
+
* | anything older | an absolute date+time in the user's locale, e.g. `Sep 10, 2026, 14:18` |
|
|
13
|
+
*
|
|
14
|
+
* The absolute tier is deliberately locale-driven, so its exact rendering is the
|
|
15
|
+
* runtime's to choose — `en-US` gives `Sep 10, 2026, 14:18` and `en-GB` gives
|
|
16
|
+
* `10 Sept 2026, 14:18`. Only the FIELDS are ours; see {@link ABSOLUTE_FORMAT_OPTIONS}.
|
|
17
|
+
*
|
|
18
|
+
* Split out as a pure function (as `format-usd.ts` was) so the tier boundaries are
|
|
19
|
+
* testable without a clock, an element or a render: every entry point takes `now`
|
|
20
|
+
* rather than reading `Date.now()` itself.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
/** Milliseconds in a minute. */
|
|
25
|
+
const MS_PER_MINUTE = 60000;
|
|
26
|
+
/** Minutes in an hour. Named for the lint rule that forbids a bare literal in an expression. */
|
|
27
|
+
const MINUTES_PER_HOUR = 60;
|
|
28
|
+
/** Milliseconds in an hour — the boundary between the relative and absolute tiers. */
|
|
29
|
+
const MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;
|
|
30
|
+
/**
|
|
31
|
+
* Field selection for the absolute tier.
|
|
32
|
+
*
|
|
33
|
+
* Exported so tests can build their expectation from the same options rather than from a
|
|
34
|
+
* literal: every field below renders differently per locale and time zone, so asserting
|
|
35
|
+
* `"Sep"` or `"10"` only asserts the machine the test happened to run on.
|
|
36
|
+
*
|
|
37
|
+
* Locale is left undefined so it follows the user's, and `hourCycle: 'h23'` pins the clock
|
|
38
|
+
* to 24-hour regardless — a locale-chosen 12-hour clock would add "am"/"pm" and widen the
|
|
39
|
+
* string next to the sender name.
|
|
40
|
+
*/
|
|
41
|
+
export const ABSOLUTE_FORMAT_OPTIONS = {
|
|
42
|
+
day: 'numeric',
|
|
43
|
+
month: 'short',
|
|
44
|
+
year: 'numeric',
|
|
45
|
+
hour: '2-digit',
|
|
46
|
+
minute: '2-digit',
|
|
47
|
+
hourCycle: 'h23',
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Built once at module load rather than per message: a transcript re-renders on every
|
|
51
|
+
* store change, and `Intl.DateTimeFormat` construction is the expensive half of the API
|
|
52
|
+
* (formatting against an existing instance is cheap).
|
|
53
|
+
*/
|
|
54
|
+
const absoluteFormatter = new Intl.DateTimeFormat(undefined, ABSOLUTE_FORMAT_OPTIONS);
|
|
55
|
+
/**
|
|
56
|
+
* Display text for a message timestamp, per the tiers above.
|
|
57
|
+
*
|
|
58
|
+
* A timestamp slightly in the future (clock skew between a server-stamped message and the
|
|
59
|
+
* browser) reads `Now` rather than a negative count — "-3 minutes ago" looks broken and
|
|
60
|
+
* tells the user nothing. That indulgence is bounded at an hour: beyond it the stamp is
|
|
61
|
+
* not skew but bad data (a machine with a badly wrong clock, a hand-edited session), and
|
|
62
|
+
* showing `Now` forever on every message would hide that. Past the bound it falls through
|
|
63
|
+
* to the absolute tier, which shows the user what the data actually says.
|
|
64
|
+
*
|
|
65
|
+
* @param iso - ISO-8601 timestamp, as stamped by the driver on append.
|
|
66
|
+
* @param now - Current epoch ms. Passed in rather than read, so tests pin the clock.
|
|
67
|
+
* @returns Display text, or `''` when there is nothing sensible to show.
|
|
68
|
+
*/
|
|
69
|
+
export function formatMessageTimestamp(iso, now) {
|
|
70
|
+
const ms = toEpochMs(iso);
|
|
71
|
+
if (ms === undefined)
|
|
72
|
+
return '';
|
|
73
|
+
const age = now - ms;
|
|
74
|
+
if (age < MS_PER_MINUTE && age > -MS_PER_HOUR)
|
|
75
|
+
return 'Now';
|
|
76
|
+
if (age >= MS_PER_MINUTE && age < MS_PER_HOUR) {
|
|
77
|
+
const minutes = Math.floor(age / MS_PER_MINUTE);
|
|
78
|
+
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
|
|
79
|
+
}
|
|
80
|
+
return absoluteFormatter.format(ms);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The absolute rendering of a stamp, whatever its age — the accessible text behind a
|
|
84
|
+
* message timestamp.
|
|
85
|
+
*
|
|
86
|
+
* Exists because the visible text is relative and a screen reader announces the element's
|
|
87
|
+
* TEXT, not its `datetime` attribute: `<time>` carries no implicit ARIA role, so the
|
|
88
|
+
* machine-readable value reaches tooling and nobody else. The template pairs this with
|
|
89
|
+
* `aria-hidden` on the relative string so assistive tech hears one unambiguous time
|
|
90
|
+
* instead of "5 minutes ago" with no anchor.
|
|
91
|
+
*
|
|
92
|
+
* @param iso - ISO-8601 timestamp, as stamped by the driver on append.
|
|
93
|
+
* @returns The absolute string, or `''` when there is nothing sensible to show.
|
|
94
|
+
*/
|
|
95
|
+
export function formatAbsoluteTimestamp(iso) {
|
|
96
|
+
const ms = toEpochMs(iso);
|
|
97
|
+
return ms === undefined ? '' : absoluteFormatter.format(ms);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The `datetime` attribute value for the rendered `<time>` element — the raw ISO string,
|
|
101
|
+
* which is what the HTML spec wants and what date-aware tooling reads. It is NOT an
|
|
102
|
+
* accessibility affordance: screen readers announce the element's text content instead.
|
|
103
|
+
*
|
|
104
|
+
* Returns `null`, not `''`, for a missing or unparseable stamp: FAST removes an attribute
|
|
105
|
+
* only for `null`/`undefined` (see `DOM.setAttribute`), so an empty string would emit
|
|
106
|
+
* `datetime=""`, which is invalid per spec and flagged by HTML validators.
|
|
107
|
+
*
|
|
108
|
+
* @param iso - ISO-8601 timestamp, as stamped by the driver on append.
|
|
109
|
+
*/
|
|
110
|
+
export function messageTimestampDateTime(iso) {
|
|
111
|
+
return toEpochMs(iso) === undefined ? null : iso;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a stamp to epoch ms, rejecting both absence and garbage.
|
|
115
|
+
*
|
|
116
|
+
* `timestamp` is optional on {@link ChatMessage} for one real reason — sessions persisted
|
|
117
|
+
* before timestamp support restore without it — and a persisted blob is host-supplied data
|
|
118
|
+
* that can also be malformed. `new Date(junk).getTime()` is `NaN`, which would otherwise
|
|
119
|
+
* flow into the arithmetic and render "NaN minutes ago".
|
|
120
|
+
*/
|
|
121
|
+
function toEpochMs(iso) {
|
|
122
|
+
if (!iso)
|
|
123
|
+
return undefined;
|
|
124
|
+
const ms = new Date(iso).getTime();
|
|
125
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
126
|
+
}
|
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import { DEFAULT_ANIMATIONS } from '../main/main.types';
|
|
2
2
|
import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a preference write is a no-op, so a setter can bail before touching the store.
|
|
5
|
+
*
|
|
6
|
+
* Load-bearing rather than an optimisation. The design-system `switch` fires `@change` when its
|
|
7
|
+
* `:checked` binding is applied on render, not only on user interaction — so an unguarded setter
|
|
8
|
+
* marks preferences "user-changed" on every render and saves config defaults, which freezes the
|
|
9
|
+
* reconciler so real stored prefs never apply on reload (GENC-1351 #1).
|
|
10
|
+
*
|
|
11
|
+
* List preferences compare order-insensitively: the per-animation switches fire the same spurious
|
|
12
|
+
* render-time `@change`, and `enabledAnimations` is a set rather than a sequence.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export function preferenceUnchanged(current, value) {
|
|
17
|
+
if (Array.isArray(current) && Array.isArray(value)) {
|
|
18
|
+
return current.length === value.length && current.every((item) => value.includes(item));
|
|
19
|
+
}
|
|
20
|
+
return current === value;
|
|
21
|
+
}
|
|
22
|
+
/** The mode used when neither the host config nor stored prefs name a valid one. */
|
|
23
|
+
export const DEFAULT_TIMESTAMP_DISPLAY = 'hover';
|
|
24
|
+
/**
|
|
25
|
+
* Every valid {@link TimestampDisplay}, for narrowing untrusted input.
|
|
26
|
+
*
|
|
27
|
+
* Keyed by the union rather than typed `readonly string[]`, so adding a mode is a compile
|
|
28
|
+
* error here. A plain string array severs the link: the new mode would type-check
|
|
29
|
+
* everywhere, then be rejected at runtime as junk and silently fall back to `'hover'` —
|
|
30
|
+
* surfacing as "my new mode doesn't stick" rather than as a type error.
|
|
31
|
+
*/
|
|
32
|
+
const TIMESTAMP_DISPLAY_MODES = {
|
|
33
|
+
off: true,
|
|
34
|
+
hover: true,
|
|
35
|
+
always: true,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Narrow an untrusted value to a {@link TimestampDisplay}, or `undefined` if it is not one.
|
|
39
|
+
*
|
|
40
|
+
* Both inputs need this: the config field is typed but comes from plain JS hosts, and the
|
|
41
|
+
* stored field is a bare `string` read back out of a persistence blob.
|
|
42
|
+
*/
|
|
43
|
+
const asTimestampDisplay = (value) => typeof value === 'string' && value in TIMESTAMP_DISPLAY_MODES
|
|
44
|
+
? value
|
|
45
|
+
: undefined;
|
|
3
46
|
/**
|
|
4
47
|
* Resolve the effective preference baseline for a fresh store, applying the
|
|
5
48
|
* precedence (lowest → highest):
|
|
@@ -20,7 +63,7 @@ import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
|
|
|
20
63
|
* @internal
|
|
21
64
|
*/
|
|
22
65
|
export function resolvePreferenceBaseline(ui, stored) {
|
|
23
|
-
var _a, _b;
|
|
66
|
+
var _a, _b, _c;
|
|
24
67
|
const u = ui !== null && ui !== void 0 ? ui : {};
|
|
25
68
|
// Layer 1 — chatConfig.ui defaults.
|
|
26
69
|
const baseline = {
|
|
@@ -28,7 +71,10 @@ export function resolvePreferenceBaseline(ui, stored) {
|
|
|
28
71
|
showThinkingSteps: u.showThinkingSteps === true,
|
|
29
72
|
showNarration: u.showNarration === true,
|
|
30
73
|
showAgentSwitchIndicator: u.showAgentSwitchIndicator === true,
|
|
31
|
-
|
|
74
|
+
// Defaults ON, unlike the booleans above — so this reads the config value when it is
|
|
75
|
+
// a valid mode and falls back to `'hover'`, rather than testing `=== true`.
|
|
76
|
+
timestampDisplay: (_a = asTimestampDisplay(u.showTimestamps)) !== null && _a !== void 0 ? _a : DEFAULT_TIMESTAMP_DISPLAY,
|
|
77
|
+
enabledAnimations: resolveExclusiveLoadingStyle((_c = (_b = u.animations) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : (u.animations ? [...DEFAULT_ANIMATIONS] : [])),
|
|
32
78
|
};
|
|
33
79
|
// Layer 2 — stored user preferences override the fields they carry.
|
|
34
80
|
if (stored) {
|
|
@@ -43,6 +89,12 @@ export function resolvePreferenceBaseline(ui, stored) {
|
|
|
43
89
|
if (stored.showAgentSwitchIndicator !== undefined) {
|
|
44
90
|
baseline.showAgentSwitchIndicator = stored.showAgentSwitchIndicator;
|
|
45
91
|
}
|
|
92
|
+
// Validated, not just presence-checked: `SessionPreferences.timestampDisplay` is a
|
|
93
|
+
// plain `string` off a host-supplied blob, so a stale or hand-edited value must fall
|
|
94
|
+
// through to the config layer instead of poking a junk mode into the store.
|
|
95
|
+
const storedMode = asTimestampDisplay(stored.timestampDisplay);
|
|
96
|
+
if (storedMode)
|
|
97
|
+
baseline.timestampDisplay = storedMode;
|
|
46
98
|
if (Array.isArray(stored.animations)) {
|
|
47
99
|
// De-dupe defensively — historical blobs may hold duplicates from the old
|
|
48
100
|
// settings-modal append bug (now fixed at the `setEnabledAnimations` source).
|
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": "15.
|
|
4
|
+
"version": "15.27.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -74,26 +74,26 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@genesislcap/foundation-testing": "15.
|
|
78
|
-
"@genesislcap/genx": "15.
|
|
79
|
-
"@genesislcap/rollup-builder": "15.
|
|
80
|
-
"@genesislcap/ts-builder": "15.
|
|
81
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
82
|
-
"@genesislcap/vite-builder": "15.
|
|
83
|
-
"@genesislcap/webpack-builder": "15.
|
|
77
|
+
"@genesislcap/foundation-testing": "15.27.0",
|
|
78
|
+
"@genesislcap/genx": "15.27.0",
|
|
79
|
+
"@genesislcap/rollup-builder": "15.27.0",
|
|
80
|
+
"@genesislcap/ts-builder": "15.27.0",
|
|
81
|
+
"@genesislcap/uvu-playwright-builder": "15.27.0",
|
|
82
|
+
"@genesislcap/vite-builder": "15.27.0",
|
|
83
|
+
"@genesislcap/webpack-builder": "15.27.0",
|
|
84
84
|
"@types/dompurify": "^3.0.5",
|
|
85
85
|
"@types/marked": "^5.0.2",
|
|
86
86
|
"esbuild": "0.25.12"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"@genesislcap/foundation-ai": "15.
|
|
90
|
-
"@genesislcap/foundation-logger": "15.
|
|
91
|
-
"@genesislcap/foundation-notifications": "15.
|
|
92
|
-
"@genesislcap/foundation-redux": "15.
|
|
93
|
-
"@genesislcap/foundation-ui": "15.
|
|
94
|
-
"@genesislcap/foundation-utils": "15.
|
|
95
|
-
"@genesislcap/rapid-design-system": "15.
|
|
96
|
-
"@genesislcap/web-core": "15.
|
|
89
|
+
"@genesislcap/foundation-ai": "15.27.0",
|
|
90
|
+
"@genesislcap/foundation-logger": "15.27.0",
|
|
91
|
+
"@genesislcap/foundation-notifications": "15.27.0",
|
|
92
|
+
"@genesislcap/foundation-redux": "15.27.0",
|
|
93
|
+
"@genesislcap/foundation-ui": "15.27.0",
|
|
94
|
+
"@genesislcap/foundation-utils": "15.27.0",
|
|
95
|
+
"@genesislcap/rapid-design-system": "15.27.0",
|
|
96
|
+
"@genesislcap/web-core": "15.27.0",
|
|
97
97
|
"dompurify": "^3.3.1",
|
|
98
98
|
"marked": "^17.0.3"
|
|
99
99
|
},
|