@genesislcap/ai-assistant 15.26.0 → 15.27.0-agent-log-submit.3
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 +1014 -110
- package/dist/ai-assistant.d.ts +236 -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 +1171 -248
- 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.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +28 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +180 -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-debug-log-note.d.ts +25 -0
- package/dist/dts/utils/resolve-debug-log-note.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.js +1 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +85 -5
- package/dist/esm/components/settings-modal/settings-modal.template.js +220 -25
- package/dist/esm/main/main.js +510 -23
- 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-debug-log-note.js +37 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Default cap on the user's note. Short by design — it is a caption for the log, not a report. */
|
|
2
|
+
export const DEBUG_LOG_NOTE_DEFAULT_MAX_LENGTH = 500;
|
|
3
|
+
/** Default prompt in the empty note. */
|
|
4
|
+
export const DEBUG_LOG_NOTE_DEFAULT_PLACEHOLDER = 'What were you doing, or what went wrong? (optional)';
|
|
5
|
+
/** The same prompt for a `required` note — "(optional)" would contradict the disabled Send. */
|
|
6
|
+
export const DEBUG_LOG_NOTE_REQUIRED_PLACEHOLDER = 'What were you doing, or what went wrong?';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve `debugLogSubmit.note` — `undefined`/`true` mean "compose box with defaults",
|
|
9
|
+
* `false` means "no compose box, send on first click" (→ `null`), and an object customises
|
|
10
|
+
* the box. Keeps the template free of the boolean-or-object dance.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDebugLogNoteConfig(config) {
|
|
13
|
+
var _a;
|
|
14
|
+
if (!config || config.note === false)
|
|
15
|
+
return null;
|
|
16
|
+
// `typeof null === 'object'`: a host building its config dynamically can hand us `null`
|
|
17
|
+
// even though the type forbids it, and that must read as "defaults", not crash.
|
|
18
|
+
const note = typeof config.note === 'object' && config.note !== null ? config.note : {};
|
|
19
|
+
// `>= 1`, not `> 0`: a fractional cap under 1 would floor to 0 and lock the textarea.
|
|
20
|
+
const maxLength = typeof note.maxLength === 'number' && note.maxLength >= 1
|
|
21
|
+
? Math.floor(note.maxLength)
|
|
22
|
+
: DEBUG_LOG_NOTE_DEFAULT_MAX_LENGTH;
|
|
23
|
+
const required = note.required === true;
|
|
24
|
+
return {
|
|
25
|
+
placeholder: (_a = note.placeholder) !== null && _a !== void 0 ? _a : (required ? DEBUG_LOG_NOTE_REQUIRED_PLACEHOLDER : DEBUG_LOG_NOTE_DEFAULT_PLACEHOLDER),
|
|
26
|
+
required,
|
|
27
|
+
maxLength,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalise the typed note for the submission: trimmed, and `undefined` when empty so a
|
|
32
|
+
* host can `message ?? fallback` without checking for whitespace.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeDebugLogNote(note) {
|
|
35
|
+
const trimmed = note.trim();
|
|
36
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
37
|
+
}
|
|
@@ -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-agent-log-submit.3",
|
|
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-agent-log-submit.3",
|
|
78
|
+
"@genesislcap/genx": "15.27.0-agent-log-submit.3",
|
|
79
|
+
"@genesislcap/rollup-builder": "15.27.0-agent-log-submit.3",
|
|
80
|
+
"@genesislcap/ts-builder": "15.27.0-agent-log-submit.3",
|
|
81
|
+
"@genesislcap/uvu-playwright-builder": "15.27.0-agent-log-submit.3",
|
|
82
|
+
"@genesislcap/vite-builder": "15.27.0-agent-log-submit.3",
|
|
83
|
+
"@genesislcap/webpack-builder": "15.27.0-agent-log-submit.3",
|
|
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-agent-log-submit.3",
|
|
90
|
+
"@genesislcap/foundation-logger": "15.27.0-agent-log-submit.3",
|
|
91
|
+
"@genesislcap/foundation-notifications": "15.27.0-agent-log-submit.3",
|
|
92
|
+
"@genesislcap/foundation-redux": "15.27.0-agent-log-submit.3",
|
|
93
|
+
"@genesislcap/foundation-ui": "15.27.0-agent-log-submit.3",
|
|
94
|
+
"@genesislcap/foundation-utils": "15.27.0-agent-log-submit.3",
|
|
95
|
+
"@genesislcap/rapid-design-system": "15.27.0-agent-log-submit.3",
|
|
96
|
+
"@genesislcap/web-core": "15.27.0-agent-log-submit.3",
|
|
97
97
|
"dompurify": "^3.3.1",
|
|
98
98
|
"marked": "^17.0.3"
|
|
99
99
|
},
|
package/sandbox/README.md
CHANGED
|
@@ -30,8 +30,8 @@ The panel covers, roughly in the order you'll want them:
|
|
|
30
30
|
| **Session menu (⋮)** | Whether a persistence provider is configured (the menu is gated on one) and `showSessionMenu`; a button to open the menu |
|
|
31
31
|
| **Context window** | **Jump to state** (the four gate states in one click), the simulated window and fill, `chatConfig.context`, a flow-in-progress toggle, live readouts of the gate / thresholds / compaction plan, and Compact |
|
|
32
32
|
| **Simulated state** | Forces `idle` / busy / cancelling / blocked / compacting / restoring onto the element |
|
|
33
|
-
| **Transcript** | Seeds an empty, short, tool-and-markdown-heavy, 28-turn, ~20k-token, or ~22k-token tool-loop conversation |
|
|
34
|
-
| **Chrome** | Header title, popout mode, suggestions, the tool-call / reasoning / narration toggles, animations |
|
|
33
|
+
| **Transcript** | Seeds an empty, short, tool-and-markdown-heavy, 28-turn, ~20k-token, or ~22k-token tool-loop conversation, and how far back to shift its timestamps |
|
|
34
|
+
| **Chrome** | Header title, popout mode, suggestions, the tool-call / reasoning / narration toggles, the timestamp mode, animations |
|
|
35
35
|
| **Stage** | Frame size (floored at 400px — see below), light/dark, remount, full reset |
|
|
36
36
|
|
|
37
37
|
Panel state persists in `localStorage`, so a reload picks up where you left off. **Reset sandbox**
|
|
@@ -122,6 +122,49 @@ A run worth doing:
|
|
|
122
122
|
far above `block` on purpose: the reserve exists so an accepted turn can spend it, and a guard at
|
|
123
123
|
the block line would kill every turn that used the headroom it was given.
|
|
124
124
|
|
|
125
|
+
## Checking message timestamps (GENC-1257)
|
|
126
|
+
|
|
127
|
+
Each message can show when it was sent, beside the sender name. **Timestamps** in the Chrome
|
|
128
|
+
group and the cog's own picker both drive the same three modes, and whichever you moved last
|
|
129
|
+
wins. Moving any *other* panel control will not disturb a choice made in the cog.
|
|
130
|
+
|
|
131
|
+
The panel control writes `chatConfig.ui.showTimestamps` **and** the element's
|
|
132
|
+
`timestampDisplay` property. That second write is what makes it work, and is worth knowing
|
|
133
|
+
about before you add a control of your own: `chatConfig.ui` is read only while the store is
|
|
134
|
+
fresh and the user has changed nothing (`applyPreferenceBaseline`), so the moment the cog is
|
|
135
|
+
touched — or a stored preference exists — a config change is correctly ignored. That is right
|
|
136
|
+
for a real host, where a user's choice should outrank a host default, and useless for a
|
|
137
|
+
harness control, which has to win.
|
|
138
|
+
|
|
139
|
+
Show tool calls / reasoning / narration work the same way, through the same
|
|
140
|
+
`DIRECT_PREFERENCE_WRITERS` table in `sandbox.ts` — add an entry there for any new panel
|
|
141
|
+
control that drives a user preference, or it will quietly stop responding the first time
|
|
142
|
+
anyone opens the cog. **Animations is deliberately not in that table**: it is a feature gate
|
|
143
|
+
rather than a display preference (unsetting it removes the cog control altogether), and it has
|
|
144
|
+
no single element property to mirror onto.
|
|
145
|
+
|
|
146
|
+
- **On hover** (the default) — hidden until the pointer is over that message row. The space is
|
|
147
|
+
still reserved, so a reveal never reflows the bubble under the cursor.
|
|
148
|
+
- **Always** — permanently visible.
|
|
149
|
+
- **Off** — not rendered, and therefore absent from the accessibility tree too.
|
|
150
|
+
|
|
151
|
+
`opacity: 0` hides from paint but not from the accessibility tree, and assistive tech has no
|
|
152
|
+
hover, so in **both** visible modes a screen reader announces a timestamp on every message.
|
|
153
|
+
It hears the absolute date+time, carried in a visually-hidden span with the relative text
|
|
154
|
+
`aria-hidden` — a `<time>` element has no implicit ARIA role, so its `datetime` attribute is
|
|
155
|
+
never announced and cannot serve as the accessible value.
|
|
156
|
+
|
|
157
|
+
The display has three tiers — "Now" under a minute, "12 minutes ago" under an hour, and a full
|
|
158
|
+
date beyond that. **Age** in the Transcript group is what makes the third reachable: the
|
|
159
|
+
fixtures stamp their turns a minute apart ending "now", so a seeded conversation spans minutes
|
|
160
|
+
and would never cross the hour boundary on its own. Shifting the whole seed back 3 hours or 3
|
|
161
|
+
days is the only way to see the absolute tier without inventing a fixture nobody else needs.
|
|
162
|
+
|
|
163
|
+
The relative labels are recomputed on a 30s tick rather than at render, because FAST evaluates
|
|
164
|
+
a binding once and an idle conversation never re-renders — without it "2 minutes ago" would
|
|
165
|
+
still read "2 minutes ago" an hour later. The ticker only runs while the element is connected
|
|
166
|
+
and the mode is not `off`.
|
|
167
|
+
|
|
125
168
|
## 400px is the narrowest supported width
|
|
126
169
|
|
|
127
170
|
The stage will not go below it — slider, drag handle and a restored panel state are all clamped —
|