@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.
Files changed (43) hide show
  1. package/dist/ai-assistant.api.json +150 -7
  2. package/dist/ai-assistant.d.ts +82 -99
  3. package/dist/chat-driver.cjs +460 -373
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +460 -373
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +682 -202
  8. package/dist/dts/components/chat-driver/chat-driver.d.ts +28 -200
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/interaction-controller.d.ts +99 -0
  11. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -0
  12. package/dist/dts/components/chat-driver/turn-recorder.d.ts +191 -0
  13. package/dist/dts/components/chat-driver/turn-recorder.d.ts.map +1 -0
  14. package/dist/dts/components/settings-modal/settings-modal.template.d.ts +20 -0
  15. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  16. package/dist/dts/main/main.d.ts +32 -1
  17. package/dist/dts/main/main.d.ts.map +1 -1
  18. package/dist/dts/main/main.styles.d.ts.map +1 -1
  19. package/dist/dts/main/main.template.d.ts +19 -0
  20. package/dist/dts/main/main.template.d.ts.map +1 -1
  21. package/dist/dts/state/ai-assistant-slice.d.ts +7 -1
  22. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  23. package/dist/dts/state/persistence/session-persistence-provider.d.ts +5 -0
  24. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  25. package/dist/dts/state/session-store.d.ts +1 -0
  26. package/dist/dts/state/session-store.d.ts.map +1 -1
  27. package/dist/dts/utils/format-message-timestamp.d.ts +77 -0
  28. package/dist/dts/utils/format-message-timestamp.d.ts.map +1 -0
  29. package/dist/dts/utils/resolve-preference-baseline.d.ts +18 -1
  30. package/dist/dts/utils/resolve-preference-baseline.d.ts.map +1 -1
  31. package/dist/esm/components/chat-driver/chat-driver.js +68 -404
  32. package/dist/esm/components/chat-driver/interaction-controller.js +235 -0
  33. package/dist/esm/components/chat-driver/turn-recorder.js +206 -0
  34. package/dist/esm/components/settings-modal/settings-modal.template.js +53 -2
  35. package/dist/esm/main/main.js +80 -17
  36. package/dist/esm/main/main.styles.js +75 -0
  37. package/dist/esm/main/main.template.js +43 -2
  38. package/dist/esm/state/ai-assistant-slice.js +5 -0
  39. package/dist/esm/utils/format-message-timestamp.js +126 -0
  40. package/dist/esm/utils/resolve-preference-baseline.js +54 -2
  41. package/package.json +16 -16
  42. package/sandbox/README.md +45 -2
  43. package/sandbox/sandbox.ts +95 -3
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 —
@@ -3,7 +3,7 @@
3
3
  * no backend, so the template, styling and responsive behaviour can be worked on
4
4
  * directly. See `sandbox/README.md`.
5
5
  */
6
- import type { ChatConfig } from '@genesislcap/foundation-ai';
6
+ import type { ChatConfig, TimestampDisplay } from '@genesislcap/foundation-ai';
7
7
  import { registerAIProviders } from '@genesislcap/foundation-ai';
8
8
  import { baseLayerLuminance, registerRapidDesignSystem } from '@genesislcap/rapid-design-system';
9
9
  import { DI } from '@genesislcap/web-core';
@@ -135,6 +135,7 @@ interface SandboxState {
135
135
  showToolCalls: boolean;
136
136
  showThinkingSteps: boolean;
137
137
  showNarration: boolean;
138
+ showTimestamps: TimestampDisplay;
138
139
  animations: boolean;
139
140
  // Context window (GENC-1567)
140
141
  contextPreset: ContextPresetName | typeof CUSTOM_PRESET;
@@ -148,6 +149,7 @@ interface SandboxState {
148
149
  // Simulated runtime
149
150
  runtime: 'idle' | 'loading' | 'cancelling' | 'blocked' | 'compacting' | 'restoring';
150
151
  transcript: TranscriptName;
152
+ transcriptAge: TranscriptAge;
151
153
  // Stage
152
154
  width: number;
153
155
  height: number;
@@ -176,6 +178,7 @@ const DEFAULT_STATE: SandboxState = {
176
178
  showToolCalls: true,
177
179
  showThinkingSteps: true,
178
180
  showNarration: true,
181
+ showTimestamps: 'hover',
179
182
  animations: true,
180
183
  // Chosen so the `huge` (~20k) and `loop` (~22k) seeds actually clear the tail
181
184
  // budget these produce (~12.8k) and so have something to compact. Verified, not
@@ -191,6 +194,7 @@ const DEFAULT_STATE: SandboxState = {
191
194
  flowInProgress: false,
192
195
  runtime: 'idle',
193
196
  transcript: 'rich',
197
+ transcriptAge: 'fresh',
194
198
  width: 480,
195
199
  height: 760,
196
200
  theme: 'dark',
@@ -254,13 +258,41 @@ let assistant!: FoundationAiAssistant;
254
258
  */
255
259
  let lastSeed = '';
256
260
 
261
+ /**
262
+ * How far back the whole seeded transcript is shifted (GENC-1257).
263
+ *
264
+ * The fixtures stamp their turns a minute apart ending "now", which exercises the
265
+ * "Now" and "N minutes ago" tiers but can never reach the absolute one — an hour of
266
+ * transcript would take sixty turns. Shifting the entire seed backwards is the only
267
+ * way to see the third tier without inventing a fixture nobody else needs.
268
+ */
269
+ type TranscriptAge = 'fresh' | 'hours' | 'days';
270
+
271
+ const MS_PER_HOUR = 3_600_000;
272
+ const MS_PER_DAY = 86_400_000;
273
+ const AGE_SHIFT_HOURS = 3;
274
+ const AGE_SHIFT_DAYS = 3;
275
+
276
+ const TRANSCRIPT_AGE_OFFSET_MS: Record<TranscriptAge, number> = {
277
+ fresh: 0,
278
+ hours: AGE_SHIFT_HOURS * MS_PER_HOUR,
279
+ days: AGE_SHIFT_DAYS * MS_PER_DAY,
280
+ };
281
+
257
282
  const seedTranscript = (s: SandboxState): void => {
258
- const fingerprint = `${s.transcript}|${s.agentCount}|${s.manualSelection}`;
283
+ const fingerprint = `${s.transcript}|${s.agentCount}|${s.manualSelection}|${s.transcriptAge}`;
259
284
  if (fingerprint === lastSeed) return;
260
285
  lastSeed = fingerprint;
261
286
  // Deferred so it lands after any driver rebuild the agents change kicked off.
262
287
  setTimeout(() => {
263
- const seed = TRANSCRIPTS[s.transcript].map((m) => ({ ...m }));
288
+ const shift = TRANSCRIPT_AGE_OFFSET_MS[s.transcriptAge];
289
+ const seed = TRANSCRIPTS[s.transcript].map((m) => ({
290
+ ...m,
291
+ // Shift rather than restamp, so the fixtures' own relative spacing survives.
292
+ ...(shift && m.timestamp
293
+ ? { timestamp: new Date(new Date(m.timestamp).getTime() - shift).toISOString() }
294
+ : {}),
295
+ }));
264
296
  (assistant as any).driver?.loadHistory?.(seed.map((m) => ({ ...m })));
265
297
  assistant.messages = seed;
266
298
  // `auto` measures the transcript, which only exists as of this tick — so the
@@ -309,6 +341,7 @@ const buildChatConfig = (s: SandboxState): ChatConfig => ({
309
341
  showToolCalls: s.showToolCalls,
310
342
  showThinkingSteps: s.showThinkingSteps,
311
343
  showNarration: s.showNarration,
344
+ showTimestamps: s.showTimestamps,
312
345
  // `undefined` disables the feature outright (no cog control, nothing enabled);
313
346
  // `userConfigurable` turns the defaults on and adds the multiselect to the cog.
314
347
  animations: s.animations ? { userConfigurable: true } : undefined,
@@ -473,6 +506,41 @@ const applyStage = (s: SandboxState): void => {
473
506
  document.body.dataset.stageTheme = s.theme;
474
507
  };
475
508
 
509
+ /**
510
+ * Panel controls that must ALSO be written straight onto the element, keyed by the state
511
+ * key that drives them.
512
+ *
513
+ * `chatConfig.ui` is consulted only while the store is fresh and the user has changed
514
+ * nothing (`applyPreferenceBaseline`), so the moment the cog is touched — or a stored
515
+ * preference exists — a config change is correctly ignored. That is right for a real host,
516
+ * where a user's choice should outrank a host default, and useless for a harness control,
517
+ * which has to win.
518
+ *
519
+ * Applied ONLY for the key that actually moved (see the `renderControls` callback), so an
520
+ * unrelated panel change cannot silently clobber a choice made in the cog. Whichever of the
521
+ * two you touched last is what you get.
522
+ *
523
+ * `animations` is deliberately absent: it is a feature gate rather than a display
524
+ * preference (`undefined` disables the feature and removes the cog control altogether), and
525
+ * it has no single element property to mirror it onto.
526
+ */
527
+ const DIRECT_PREFERENCE_WRITERS: Partial<
528
+ Record<keyof SandboxState & string, (s: SandboxState) => void>
529
+ > = {
530
+ showTimestamps: (s) => {
531
+ assistant.timestampDisplay = s.showTimestamps;
532
+ },
533
+ showToolCalls: (s) => {
534
+ assistant.showToolCalls = s.showToolCalls;
535
+ },
536
+ showThinkingSteps: (s) => {
537
+ assistant.showThinkingSteps = s.showThinkingSteps;
538
+ },
539
+ showNarration: (s) => {
540
+ assistant.showNarration = s.showNarration;
541
+ },
542
+ };
543
+
476
544
  function apply(): void {
477
545
  assistant.headerTitle = state.headerTitle;
478
546
  assistant.placeholder = state.placeholder;
@@ -783,6 +851,17 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
783
851
  { value: 'wall', label: 'Wall (~24k in one indivisible tool result)' },
784
852
  ],
785
853
  },
854
+ {
855
+ kind: 'select',
856
+ key: 'transcriptAge',
857
+ label: 'Age',
858
+ options: [
859
+ { value: 'fresh', label: 'Just now' },
860
+ { value: 'hours', label: '3 hours ago' },
861
+ { value: 'days', label: '3 days ago' },
862
+ ],
863
+ hint: 'Shifts the whole seed back. The fixtures span minutes, so this is the only way to reach the absolute date tier.',
864
+ },
786
865
  {
787
866
  kind: 'action',
788
867
  label: 'Re-seed transcript',
@@ -826,6 +905,17 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
826
905
  { kind: 'toggle', key: 'showToolCalls', label: 'Show tool calls' },
827
906
  { kind: 'toggle', key: 'showThinkingSteps', label: 'Show reasoning' },
828
907
  { kind: 'toggle', key: 'showNarration', label: 'Show narration' },
908
+ {
909
+ kind: 'select',
910
+ key: 'showTimestamps',
911
+ label: 'Timestamps',
912
+ options: [
913
+ { value: 'off', label: 'Off' },
914
+ { value: 'hover', label: 'On hover' },
915
+ { value: 'always', label: 'Always' },
916
+ ],
917
+ hint: 'Also settable from the cog. Whichever you moved last wins; moving other panel controls will not disturb a cog choice.',
918
+ },
829
919
  { kind: 'toggle', key: 'animations', label: 'Animations' },
830
920
  ],
831
921
  },
@@ -890,6 +980,8 @@ const paint = (): void => {
890
980
  state.contextPreset = CUSTOM_PRESET;
891
981
  }
892
982
  apply();
983
+ // Only for the control that actually moved — see DIRECT_PREFERENCE_WRITERS.
984
+ if (key) DIRECT_PREFERENCE_WRITERS[key]?.(state);
893
985
  });
894
986
  };
895
987