@genesislcap/ai-assistant 15.25.1 → 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.
Files changed (52) hide show
  1. package/dist/ai-assistant.api.json +1014 -110
  2. package/dist/ai-assistant.d.ts +236 -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 +1171 -248
  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.d.ts.map +1 -1
  15. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  16. package/dist/dts/components/settings-modal/settings-modal.template.d.ts +28 -0
  17. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  18. package/dist/dts/index.d.ts +1 -0
  19. package/dist/dts/index.d.ts.map +1 -1
  20. package/dist/dts/main/main.d.ts +180 -1
  21. package/dist/dts/main/main.d.ts.map +1 -1
  22. package/dist/dts/main/main.styles.d.ts.map +1 -1
  23. package/dist/dts/main/main.template.d.ts +19 -0
  24. package/dist/dts/main/main.template.d.ts.map +1 -1
  25. package/dist/dts/state/ai-assistant-slice.d.ts +7 -1
  26. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  27. package/dist/dts/state/persistence/session-persistence-provider.d.ts +5 -0
  28. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  29. package/dist/dts/state/session-store.d.ts +1 -0
  30. package/dist/dts/state/session-store.d.ts.map +1 -1
  31. package/dist/dts/utils/format-message-timestamp.d.ts +77 -0
  32. package/dist/dts/utils/format-message-timestamp.d.ts.map +1 -0
  33. package/dist/dts/utils/resolve-debug-log-note.d.ts +25 -0
  34. package/dist/dts/utils/resolve-debug-log-note.d.ts.map +1 -0
  35. package/dist/dts/utils/resolve-preference-baseline.d.ts +18 -1
  36. package/dist/dts/utils/resolve-preference-baseline.d.ts.map +1 -1
  37. package/dist/esm/components/chat-driver/chat-driver.js +68 -404
  38. package/dist/esm/components/chat-driver/interaction-controller.js +235 -0
  39. package/dist/esm/components/chat-driver/turn-recorder.js +206 -0
  40. package/dist/esm/components/settings-modal/settings-modal.js +1 -0
  41. package/dist/esm/components/settings-modal/settings-modal.styles.js +85 -5
  42. package/dist/esm/components/settings-modal/settings-modal.template.js +220 -25
  43. package/dist/esm/main/main.js +510 -23
  44. package/dist/esm/main/main.styles.js +75 -0
  45. package/dist/esm/main/main.template.js +43 -2
  46. package/dist/esm/state/ai-assistant-slice.js +5 -0
  47. package/dist/esm/utils/format-message-timestamp.js +126 -0
  48. package/dist/esm/utils/resolve-debug-log-note.js +37 -0
  49. package/dist/esm/utils/resolve-preference-baseline.js +54 -2
  50. package/package.json +16 -16
  51. package/sandbox/README.md +45 -2
  52. package/sandbox/sandbox.ts +95 -3
@@ -58,7 +58,8 @@ 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 { normalizeDebugLogNote, resolveDebugLogNoteConfig, } from '../utils/resolve-debug-log-note';
62
+ import { DEFAULT_TIMESTAMP_DISPLAY, preferenceUnchanged, resolvePreferenceBaseline, } from '../utils/resolve-preference-baseline';
62
63
  import { stripAgentHandlers } from '../utils/strip-agent-handlers';
63
64
  import { emptyUsage, sumUsage, totalTokens } from '../utils/sum-usage';
64
65
  import { AttachmentController } from './attachment-controller';
@@ -92,6 +93,33 @@ export const genesisIconSvg = `<svg viewBox="-4 0 30 26" fill="none" xmlns="http
92
93
  function sanitizeIconMarkup(markup) {
93
94
  return DOMPurify.sanitize(markup, { USE_PROFILES: { svg: true, svgFilters: true } });
94
95
  }
96
+ /**
97
+ * Await `outcome` (a promise, or a sync return) but give up after `ms`, rejecting with a
98
+ * named error. Used to bound a host callback we cannot otherwise trust to settle.
99
+ */
100
+ function settleWithin(outcome, ms, what) {
101
+ return __awaiter(this, void 0, void 0, function* () {
102
+ let timer;
103
+ const timeout = new Promise((_, reject) => {
104
+ timer = setTimeout(() => reject(new Error(`${what} did not settle within ${ms}ms`)), ms);
105
+ });
106
+ try {
107
+ yield Promise.race([Promise.resolve(outcome), timeout]);
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ }
112
+ });
113
+ }
114
+ /**
115
+ * How long the agent-log Send button shows its 'Sent'/'Failed' outcome before
116
+ * returning to idle — long enough to read, short enough that a retry is never blocked.
117
+ */
118
+ const DEBUG_LOG_SUBMIT_RESULT_FLASH_MS = 4000;
119
+ /** Default ceiling on the host's `debugLogSubmit.send` before the submission is failed. */
120
+ const DEBUG_LOG_SUBMIT_TIMEOUT_MS = 30000;
121
+ /** Announce the remaining note length once this few characters are left. */
122
+ const DEBUG_LOG_NOTE_REMAINING_WARN = 20;
95
123
  /**
96
124
  * Reporter id for this element's OWN session persister in `brokenSources` (GENC-1511).
97
125
  *
@@ -114,6 +142,14 @@ const DEFAULT_PERSISTENCE_SOURCE = 'host';
114
142
  * cannot drift apart.
115
143
  */
116
144
  const BLOCKED_PLACEHOLDER = 'AI usage limit reached';
145
+ /**
146
+ * How often the transcript's relative timestamps are recomputed (GENC-1257).
147
+ *
148
+ * The relative tier changes at minute granularity, so a 30s tick caps how stale a label
149
+ * can look at half a minute — close enough that a user watching one never sees it lag,
150
+ * without a per-second timer. The work per tick is one string format per visible row.
151
+ */
152
+ const TIMESTAMP_CLOCK_INTERVAL_MS = 30000;
117
153
  /**
118
154
  * Budget formatting helpers, re-exported so they stay on this module's public
119
155
  * surface (`src/index.ts` does `export * from './main/main'`). They live in
@@ -276,6 +312,16 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
276
312
  this.persistence = {};
277
313
  /** When set, enables Redux DevTools for this instance's session store. */
278
314
  this.debugRedux = false;
315
+ /**
316
+ * Epoch ms the transcript's relative timestamps are measured against.
317
+ *
318
+ * A relative label is only correct at the instant it renders: FAST evaluates the
319
+ * binding once, and an idle conversation never re-renders, so "2 minutes ago" would
320
+ * still read "2 minutes ago" an hour later. Making the formatter read this observable
321
+ * instead of `Date.now()` gives every row one shared, invalidatable clock — a tick
322
+ * re-evaluates them all.
323
+ */
324
+ this.timestampClock = Date.now();
279
325
  // ---- Transient UI state (stays as @observable on the component) ----
280
326
  this._suggestionsGeneration = 0;
281
327
  /** Whether the loading spinner is currently visible. Controlled by the loading delay timer. */
@@ -288,6 +334,34 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
288
334
  * element has its own settings modal.
289
335
  */
290
336
  this.downloadingDebugLog = false;
337
+ /**
338
+ * True while `submitDebugLog` is building and handing off the log. Same role as
339
+ * `downloadingDebugLog` for the Send button: the lifetime-stitch and the host's
340
+ * `send` callback are async, so the button needs a visible in-flight state.
341
+ */
342
+ this.submittingDebugLog = false;
343
+ /**
344
+ * Outcome of the last log submission, shown briefly on the button ('Sent' /
345
+ * 'Failed — retry') then cleared by `setDebugLogSubmitResult`'s timer. `null` when idle.
346
+ */
347
+ this.debugLogSubmitResult = null;
348
+ /**
349
+ * Whether the agent-log compose box (note + Send/Cancel) is open. Opened by the row's
350
+ * Send button when `debugLogSubmit.note` is not `false`; closed by Cancel or a
351
+ * successful send. Stays open after a failure so the typed note is not lost.
352
+ */
353
+ this.debugLogComposeOpen = false;
354
+ /** The note typed in the compose box, raw (trimmed only at submit time). */
355
+ this.debugLogNote = '';
356
+ /** How long 'Sent' shows before idle. Overridable so tests need not wait 4 seconds. */
357
+ this.debugLogSubmitResultFlashMs = DEBUG_LOG_SUBMIT_RESULT_FLASH_MS;
358
+ /**
359
+ * Bumped by `resetDebugLogSubmitState`. An in-flight `submitDebugLog` captures it and
360
+ * drops its continuation when a reset (disconnect, session switch, Clear) happened
361
+ * while it awaited — otherwise the stale submission would write state, arm a timer on
362
+ * a torn-down element, or report 'Sent' into a session it never belonged to.
363
+ */
364
+ this.debugLogSubmitGeneration = 0;
291
365
  /**
292
366
  * GENC-1511 — persistence has failed repeatedly and it is no longer saving. Drives a BLOCKING
293
367
  * modal with a single Reload action: the whole failure class this ticket exists for is one the
@@ -622,12 +696,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
622
696
  }
623
697
  set showToolCalls(value) {
624
698
  var _a;
625
- // Idempotent: a same-value set is a no-op. The design-system switch fires
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)
699
+ if (preferenceUnchanged(this.showToolCalls, value))
631
700
  return;
632
701
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowToolCalls(value);
633
702
  this.persistPreferences();
@@ -639,8 +708,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
639
708
  }
640
709
  set showThinkingSteps(value) {
641
710
  var _a;
642
- if (this.showThinkingSteps === value)
643
- return; // idempotent — see showToolCalls
711
+ if (preferenceUnchanged(this.showThinkingSteps, value))
712
+ return;
644
713
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowThinkingSteps(value);
645
714
  this.persistPreferences();
646
715
  }
@@ -651,8 +720,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
651
720
  }
652
721
  set showNarration(value) {
653
722
  var _a;
654
- if (this.showNarration === value)
655
- return; // idempotent — see showToolCalls
723
+ if (preferenceUnchanged(this.showNarration, value))
724
+ return;
656
725
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowNarration(value);
657
726
  this.persistPreferences();
658
727
  }
@@ -663,11 +732,55 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
663
732
  }
664
733
  set showAgentSwitchIndicator(value) {
665
734
  var _a;
666
- if (this.showAgentSwitchIndicator === value)
667
- return; // idempotent — see showToolCalls
735
+ if (preferenceUnchanged(this.showAgentSwitchIndicator, value))
736
+ return;
668
737
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setShowAgentSwitchIndicator(value);
669
738
  this.persistPreferences();
670
739
  }
740
+ /**
741
+ * When each message shows its timestamp — `'off'`, `'hover'` (default) or `'always'`.
742
+ *
743
+ * Falls back to `DEFAULT_TIMESTAMP_DISPLAY` rather than `'off'` when there is no
744
+ * store yet: this preference defaults ON, so the pre-store render must match the state
745
+ * the baseline is about to resolve to, or timestamps would visibly pop in on connect.
746
+ */
747
+ get timestampDisplay() {
748
+ var _a, _b;
749
+ return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.timestampDisplay) !== null && _b !== void 0 ? _b : DEFAULT_TIMESTAMP_DISPLAY;
750
+ }
751
+ set timestampDisplay(value) {
752
+ var _a;
753
+ if (preferenceUnchanged(this.timestampDisplay, value))
754
+ return;
755
+ (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setTimestampDisplay(value);
756
+ this.persistPreferences();
757
+ // The mode decides whether the clock needs to run at all.
758
+ this.syncTimestampClock();
759
+ }
760
+ /**
761
+ * Start or stop the timestamp clock to match the current mode and connection state.
762
+ *
763
+ * Idempotent, and safe to call from either lifecycle end — it is driven from
764
+ * `connectedCallback`, `disconnectedCallback` and the `timestampDisplay` setter, which
765
+ * can fire in any order. Nothing runs while the mode is `'off'` (no binding reads the
766
+ * clock) or while disconnected (a timer outliving its element is a leak, and a
767
+ * popped-out view disconnects without the session ending).
768
+ */
769
+ syncTimestampClock() {
770
+ const wanted = this._sessionActive && this.timestampDisplay !== 'off';
771
+ if (wanted === !!this._timestampClockTimer)
772
+ return;
773
+ if (!wanted) {
774
+ clearInterval(this._timestampClockTimer);
775
+ this._timestampClockTimer = undefined;
776
+ return;
777
+ }
778
+ // Re-sync immediately: the element may have sat disconnected across several ticks.
779
+ this.timestampClock = Date.now();
780
+ this._timestampClockTimer = setInterval(() => {
781
+ this.timestampClock = Date.now();
782
+ }, TIMESTAMP_CLOCK_INTERVAL_MS);
783
+ }
671
784
  /** Currently enabled animations. */
672
785
  get enabledAnimations() {
673
786
  var _a, _b;
@@ -675,10 +788,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
675
788
  }
676
789
  set enabledAnimations(value) {
677
790
  var _a;
678
- // Idempotent (order-insensitive) — see showToolCalls: the per-animation switches
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)))
791
+ if (preferenceUnchanged(this.enabledAnimations, value))
682
792
  return;
683
793
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setEnabledAnimations(value);
684
794
  this.persistPreferences();
@@ -2062,6 +2172,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2062
2172
  window.addEventListener('pagehide', this._handlePageHide);
2063
2173
  logger.debug('FoundationAiAssistant connected');
2064
2174
  this._sessionActive = true;
2175
+ // After `_sessionActive`, which `syncTimestampClock` reads as "connected".
2176
+ this.syncTimestampClock();
2065
2177
  }
2066
2178
  /**
2067
2179
  * Switch this element to a different session **in place**, without a page reload
@@ -2092,6 +2204,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2092
2204
  return;
2093
2205
  }
2094
2206
  this._deferredSwitchToKey = undefined;
2207
+ // A note typed against the outgoing conversation must not caption the incoming one's
2208
+ // log, and an outcome flash belongs to the session that produced it.
2209
+ this.resetDebugLogSubmitState();
2095
2210
  // Cancel the outgoing session's debounced autosave FIRST, so it can't land in
2096
2211
  // the incoming session's file once `_sessionRef` swaps below.
2097
2212
  (_b = getPersister(oldKey)) === null || _b === void 0 ? void 0 : _b.cancelPendingSave();
@@ -2151,7 +2266,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2151
2266
  this.costLedger.flush();
2152
2267
  super.disconnectedCallback();
2153
2268
  this._sessionActive = false;
2269
+ // After `_sessionActive` is cleared, so the sync sees "disconnected" and stops.
2270
+ this.syncTimestampClock();
2154
2271
  this.stopLoadingTimer();
2272
+ this.resetDebugLogSubmitState();
2155
2273
  this.state = 'idle';
2156
2274
  // Note: `compacting`/`restoring` are NOT cleared here. They're written through the
2157
2275
  // in-flight op's captured session store (see `initPersistenceForNewStore` /
@@ -2740,6 +2858,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2740
2858
  showThinkingSteps: this.showThinkingSteps,
2741
2859
  showNarration: this.showNarration,
2742
2860
  showAgentSwitchIndicator: this.showAgentSwitchIndicator,
2861
+ timestampDisplay: this.timestampDisplay,
2743
2862
  animations: [...this.enabledAnimations],
2744
2863
  };
2745
2864
  }
@@ -2800,6 +2919,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2800
2919
  this.showThinkingSteps = resolved.showThinkingSteps;
2801
2920
  this.showNarration = resolved.showNarration;
2802
2921
  this.showAgentSwitchIndicator = resolved.showAgentSwitchIndicator;
2922
+ this.timestampDisplay = resolved.timestampDisplay;
2803
2923
  this.enabledAnimations = resolved.enabledAnimations;
2804
2924
  }
2805
2925
  finally {
@@ -2908,6 +3028,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2908
3028
  if (this.driver)
2909
3029
  this.teardownDriver(key);
2910
3030
  (_g = this._sessionRef) === null || _g === void 0 ? void 0 : _g.actions.aiAssistant.resetSession();
3031
+ // Same reason as in `switchSession`: the compose box and its note were about the
3032
+ // conversation just discarded.
3033
+ this.resetDebugLogSubmitState();
2911
3034
  // `resetSession` zeroes the store-backed `contextTokens`; the gate's held copy
2912
3035
  // is ours to drop. Leaving it would carry a block into a conversation that no
2913
3036
  // longer exists — the one thing `contextGate` must never do (GENC-1582).
@@ -3080,6 +3203,145 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3080
3203
  get settingsSessionTokensVisible() {
3081
3204
  return this.settings.sessionTokensVisible;
3082
3205
  }
3206
+ /**
3207
+ * Whether the settings "Agent log" section renders — shown when either action is
3208
+ * enabled: the download button (`allowDebugDownload`) or the host-transport Send
3209
+ * button (`debugLogSubmit`). Each button is additionally gated on its own flag
3210
+ * inside the section, so a host can enable one without the other.
3211
+ */
3212
+ get settingsAgentLogSectionVisible() {
3213
+ const ui = this.chatConfig.ui;
3214
+ return (ui === null || ui === void 0 ? void 0 : ui.allowDebugDownload) === true || (ui === null || ui === void 0 ? void 0 : ui.debugLogSubmit) != null;
3215
+ }
3216
+ /**
3217
+ * The resolved compose-box settings for the agent-log Send button, or `null` when the
3218
+ * host opted out (`debugLogSubmit.note: false`) and the button sends on first click.
3219
+ */
3220
+ get debugLogNoteConfig() {
3221
+ var _a;
3222
+ return resolveDebugLogNoteConfig((_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.debugLogSubmit);
3223
+ }
3224
+ /**
3225
+ * Whether the typed note counts as empty — the ONE emptiness rule, shared by the
3226
+ * compose box's disabled state and its "required" hint so the two cannot drift apart.
3227
+ */
3228
+ get debugLogNoteMissing() {
3229
+ return normalizeDebugLogNote(this.debugLogNote) === undefined;
3230
+ }
3231
+ /** The Send button's idle label, as the host configured it. */
3232
+ get debugLogSubmitLabel() {
3233
+ var _a, _b, _c;
3234
+ return (_c = (_b = (_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.debugLogSubmit) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : 'Send';
3235
+ }
3236
+ /**
3237
+ * Label on the agent-log row's Send button. While the compose box is open the row
3238
+ * button is just the (disabled) trigger, so it keeps its idle label; otherwise it
3239
+ * reflects the in-flight / flashed outcome of the last submission.
3240
+ */
3241
+ get debugLogSubmitButtonLabel() {
3242
+ if (this.debugLogComposeOpen)
3243
+ return this.debugLogSubmitLabel;
3244
+ if (this.submittingDebugLog)
3245
+ return 'Sending…';
3246
+ if (this.debugLogSubmitResult === 'sent')
3247
+ return 'Sent';
3248
+ if (this.debugLogSubmitResult === 'error')
3249
+ return 'Failed — retry';
3250
+ return this.debugLogSubmitLabel;
3251
+ }
3252
+ /**
3253
+ * The compose box's footer hint: a failure first, then the "required" nudge, else the
3254
+ * live counter. Visual only — the same information reaches assistive technology via
3255
+ * `debugLogNoteAriaLabel` (counter, required) and `debugLogSubmitAnnouncement` (outcome),
3256
+ * because IDREF attributes such as `aria-describedby` cannot point across the
3257
+ * design-system text-area's shadow boundary.
3258
+ */
3259
+ get debugLogComposeHint() {
3260
+ var _a;
3261
+ if (this.debugLogSubmitResult === 'error')
3262
+ return 'Sending failed — try again.';
3263
+ if (((_a = this.debugLogNoteConfig) === null || _a === void 0 ? void 0 : _a.required) && this.debugLogNoteMissing) {
3264
+ return 'A short note is required.';
3265
+ }
3266
+ return this.debugLogNoteCounter;
3267
+ }
3268
+ /**
3269
+ * Accessible name for the note. Deliberately STATIC apart from "(required)": a name
3270
+ * that changed per keystroke would be re-read in full by NVDA on every key and never
3271
+ * updated by JAWS. The running count reaches AT through `debugLogSubmitAnnouncement`
3272
+ * at the thresholds where it matters.
3273
+ */
3274
+ get debugLogNoteAriaLabel() {
3275
+ const note = this.debugLogNoteConfig;
3276
+ const required = (note === null || note === void 0 ? void 0 : note.required) ? ' (required)' : '';
3277
+ const max = note ? `, maximum ${note.maxLength} characters` : '';
3278
+ return `Note to send with the agent log${required}${max}.`;
3279
+ }
3280
+ /**
3281
+ * Text for the always-present live region beside the agent-log row, so the outcome of
3282
+ * a submission is announced even when the compose box is closed or opted out
3283
+ * (`note: false`), where the only visible change is the button label.
3284
+ */
3285
+ get debugLogSubmitAnnouncement() {
3286
+ var _a;
3287
+ if (this.submittingDebugLog)
3288
+ return 'Sending the agent log.';
3289
+ if (this.debugLogSubmitResult === 'sent')
3290
+ return 'Agent log sent.';
3291
+ if (this.debugLogSubmitResult === 'error')
3292
+ return 'Sending the agent log failed. Try again.';
3293
+ // The note's length, only as it nears the cap: `maxlength` is invisible to AT, so at
3294
+ // the limit keystrokes would be silently dropped with nothing changing anywhere.
3295
+ const max = (_a = this.debugLogNoteConfig) === null || _a === void 0 ? void 0 : _a.maxLength;
3296
+ if (this.debugLogComposeOpen && max) {
3297
+ const remaining = max - this.debugLogNote.length;
3298
+ if (remaining <= 0) {
3299
+ return `Note is at its ${max} character limit; further typing is dropped.`;
3300
+ }
3301
+ if (remaining <= DEBUG_LOG_NOTE_REMAINING_WARN)
3302
+ return `${remaining} characters remaining.`;
3303
+ }
3304
+ return '';
3305
+ }
3306
+ /**
3307
+ * The agent-log row's description, phrased for whichever actions the host enabled.
3308
+ * Send is called out as transmitting: Download keeps the log on the user's machine,
3309
+ * Send hands the full conversation to wherever the host's transport delivers it.
3310
+ */
3311
+ get debugLogRowDescription() {
3312
+ var _a;
3313
+ const ui = this.chatConfig.ui;
3314
+ const download = (ui === null || ui === void 0 ? void 0 : ui.allowDebugDownload) === true;
3315
+ const send = (ui === null || ui === void 0 ? void 0 : ui.debugLogSubmit) != null;
3316
+ if (send && ((_a = ui === null || ui === void 0 ? void 0 : ui.debugLogSubmit) === null || _a === void 0 ? void 0 : _a.description))
3317
+ return ui.debugLogSubmit.description;
3318
+ const timeline = "a structured debug timeline of this assistant's activity";
3319
+ // What actually travels (see `ChatDebugLogSubmission.logJson`): the lifetime stream,
3320
+ // not just this conversation — say so where the user commits to it.
3321
+ const transmits = 'transmits the full lifetime log — every conversation held here, including cleared ' +
3322
+ 'ones, with attachments, system prompts and agent configuration.';
3323
+ if (download && send)
3324
+ return `Export ${timeline}, or send it to support. Sending ${transmits}`;
3325
+ if (send)
3326
+ return `Send ${timeline} to support. This ${transmits}`;
3327
+ return "Export a structured debug timeline of this session's agent activity.";
3328
+ }
3329
+ /**
3330
+ * Whether the compose box's Send button is inert: a send is already in flight, or the
3331
+ * host made the note required and it is still blank.
3332
+ */
3333
+ get debugLogSendDisabled() {
3334
+ var _a;
3335
+ if (this.submittingDebugLog)
3336
+ return true;
3337
+ return ((_a = this.debugLogNoteConfig) === null || _a === void 0 ? void 0 : _a.required) === true && this.debugLogNoteMissing;
3338
+ }
3339
+ /** Live "typed / max" counter for the compose box. */
3340
+ get debugLogNoteCounter() {
3341
+ var _a;
3342
+ const max = (_a = this.debugLogNoteConfig) === null || _a === void 0 ? void 0 : _a.maxLength;
3343
+ return max ? `${this.debugLogNote.length}/${max}` : '';
3344
+ }
3083
3345
  /** Whether the chat header (and settings cog) should render. */
3084
3346
  get settingsCogVisible() {
3085
3347
  return this.settings.cogVisible;
@@ -3263,15 +3525,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3263
3525
  }
3264
3526
  downloadDebugLog() {
3265
3527
  return __awaiter(this, void 0, void 0, function* () {
3266
- var _a, _b, _c;
3267
3528
  // Guard re-entry: the button is disabled while this runs, but a programmatic
3268
3529
  // or double-fire can't kick off a second stitch either.
3269
3530
  if (this.downloadingDebugLog)
3270
3531
  return;
3271
- const timestamp = new Date().toISOString().replace(/:/g, '-');
3272
- const agentName = ((_c = (_b = (_a = this.activeAgent) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.headerTitle) !== null && _c !== void 0 ? _c : 'chat')
3273
- .toLowerCase()
3274
- .replace(/\s+/g, '-');
3532
+ const filename = this.debugLogFilename();
3275
3533
  this.downloadingDebugLog = true;
3276
3534
  try {
3277
3535
  const payload = yield this.buildDownloadLog();
@@ -3279,7 +3537,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3279
3537
  const url = URL.createObjectURL(blob);
3280
3538
  const a = document.createElement('a');
3281
3539
  a.href = url;
3282
- a.download = `${agentName}-${timestamp}.json`;
3540
+ a.download = filename;
3283
3541
  a.click();
3284
3542
  URL.revokeObjectURL(url);
3285
3543
  }
@@ -3288,6 +3546,187 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3288
3546
  }
3289
3547
  });
3290
3548
  }
3549
+ /**
3550
+ * Hand the agent log to the host's `ui.debugLogSubmit.send` callback — the Send
3551
+ * button next to Download. Reuses `buildDownloadLog`, so the submitted log is
3552
+ * byte-identical to what Download saves (lifetime-stitched when the provider
3553
+ * persists diagnostics). The assistant is vendor-agnostic: the callback owns the
3554
+ * transport (Sentry feedback, a support API, an upload endpoint). Outcome lands in
3555
+ * `debugLogSubmitResult` so the button can flash 'Sent'/'Failed'; failures also go
3556
+ * to the logger with the cause.
3557
+ */
3558
+ submitDebugLog() {
3559
+ return __awaiter(this, void 0, void 0, function* () {
3560
+ var _a, _b;
3561
+ if (this.submittingDebugLog || this.debugLogSendDisabled)
3562
+ return;
3563
+ const config = (_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.debugLogSubmit;
3564
+ if (!config)
3565
+ return;
3566
+ const generation = this.debugLogSubmitGeneration;
3567
+ const agentName = this.debugLogAgentName;
3568
+ const filename = this.debugLogFilename();
3569
+ // Read at click time, BEFORE the async stitch: the user pressed Send on this note.
3570
+ // Only a note the user could actually type travels with the log: with the compose box
3571
+ // opted out there is no note, whatever a stale `debugLogNote` might hold.
3572
+ const message = this.debugLogNoteConfig === null ? undefined : normalizeDebugLogNote(this.debugLogNote);
3573
+ this.submittingDebugLog = true;
3574
+ try {
3575
+ const payload = yield this.buildDownloadLog();
3576
+ if (generation !== this.debugLogSubmitGeneration)
3577
+ return;
3578
+ yield settleWithin(config.send({
3579
+ logJson: JSON.stringify(payload, null, 2),
3580
+ filename,
3581
+ agentName,
3582
+ host: window.location.host,
3583
+ message,
3584
+ }), (_b = config.timeoutMs) !== null && _b !== void 0 ? _b : DEBUG_LOG_SUBMIT_TIMEOUT_MS, 'debugLogSubmit.send');
3585
+ if (generation !== this.debugLogSubmitGeneration)
3586
+ return;
3587
+ // Success consumes the note; a failure (below) keeps the box open with it intact.
3588
+ this.debugLogComposeOpen = false;
3589
+ this.debugLogNote = '';
3590
+ this.setDebugLogSubmitResult('sent');
3591
+ this.restoreDebugLogTriggerFocus();
3592
+ }
3593
+ catch (e) {
3594
+ if (generation !== this.debugLogSubmitGeneration)
3595
+ return;
3596
+ logger.error('Submitting agent log failed:', e);
3597
+ // Persists (no flash timer) until the user acts — see `setDebugLogSubmitResult`.
3598
+ this.setDebugLogSubmitResult('error');
3599
+ this.restoreDebugLogRetryFocus();
3600
+ }
3601
+ finally {
3602
+ if (generation === this.debugLogSubmitGeneration)
3603
+ this.submittingDebugLog = false;
3604
+ }
3605
+ });
3606
+ }
3607
+ /** The agent name stamped on the log: the active agent, else the header title. */
3608
+ get debugLogAgentName() {
3609
+ var _a, _b, _c;
3610
+ return (_c = (_b = (_a = this.activeAgent) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.headerTitle) !== null && _c !== void 0 ? _c : 'chat';
3611
+ }
3612
+ /**
3613
+ * Filename for the agent log — the ONE derivation Download saves under and Send hands
3614
+ * to the host, so `ChatDebugLogSubmission.filename`'s "same stem as the download"
3615
+ * promise is enforced rather than relying on two copies agreeing.
3616
+ */
3617
+ debugLogFilename() {
3618
+ const stem = this.debugLogAgentName.toLowerCase().replace(/\s+/g, '-');
3619
+ return `${stem}-${new Date().toISOString().replace(/:/g, '-')}.json`;
3620
+ }
3621
+ /**
3622
+ * Back to idle: nothing in flight, no flash, no open compose box, no note. Called on
3623
+ * disconnect and on a session switch / Clear (a note typed against one conversation
3624
+ * must not caption the next one's log). On disconnect — the
3625
+ * flash timer would otherwise fire against a torn-down element, and clearing ONLY the
3626
+ * timer would strand `debugLogSubmitResult` at 'sent'/'error' for good on a pop-out /
3627
+ * pop-in (nothing else resets it), with the box remounting open on a stale note.
3628
+ */
3629
+ resetDebugLogSubmitState() {
3630
+ this.debugLogSubmitGeneration += 1;
3631
+ clearTimeout(this.debugLogSubmitResultTimer);
3632
+ this.debugLogSubmitResultTimer = undefined;
3633
+ // Including the in-flight flag: every escape route (Send, Cancel, the note, the row
3634
+ // button) is gated on it, so a host transport that never settles would otherwise
3635
+ // wedge the feature until reload.
3636
+ this.submittingDebugLog = false;
3637
+ this.debugLogSubmitResult = null;
3638
+ this.debugLogComposeOpen = false;
3639
+ this.debugLogNote = '';
3640
+ }
3641
+ /**
3642
+ * The row's Send button. With a compose box configured (the default) it opens the box
3643
+ * and focuses the note so the user can caption the log; with `note: false` it sends
3644
+ * straight away, as before.
3645
+ */
3646
+ openDebugLogCompose() {
3647
+ if (this.submittingDebugLog)
3648
+ return;
3649
+ if (this.debugLogNoteConfig === null) {
3650
+ void this.submitDebugLog();
3651
+ return;
3652
+ }
3653
+ // Opening again resets a lingering 'Failed' flash so the box starts clean.
3654
+ clearTimeout(this.debugLogSubmitResultTimer);
3655
+ this.debugLogSubmitResultTimer = undefined;
3656
+ this.debugLogSubmitResult = null;
3657
+ this.debugLogComposeOpen = true;
3658
+ // Next frame so the `when` block has rendered the text-area. The design-system
3659
+ // text-area does not delegate focus (see `maybeAutoFocusChatInput`), so drill in.
3660
+ requestAnimationFrame(() => {
3661
+ var _a, _b, _c;
3662
+ if (!this.debugLogComposeOpen || !this.isConnected)
3663
+ return;
3664
+ const el = this.debugLogNoteEl;
3665
+ (_c = ((_b = (_a = el === null || el === void 0 ? void 0 : el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('textarea')) !== null && _b !== void 0 ? _b : el)) === null || _c === void 0 ? void 0 : _c.focus();
3666
+ });
3667
+ }
3668
+ /** The note's `@input`: track the text, and let typing dismiss a standing failure. */
3669
+ noteDebugLogInput(value) {
3670
+ this.debugLogNote = value;
3671
+ if (this.debugLogSubmitResult === 'error')
3672
+ this.debugLogSubmitResult = null;
3673
+ }
3674
+ /** Close the compose box and discard the typed note. */
3675
+ cancelDebugLogCompose() {
3676
+ if (this.submittingDebugLog)
3677
+ return;
3678
+ this.debugLogComposeOpen = false;
3679
+ this.debugLogNote = '';
3680
+ this.restoreDebugLogTriggerFocus();
3681
+ }
3682
+ /**
3683
+ * Closing the compose box unmounts the focused text-area, which drops focus to the
3684
+ * document body and strands keyboard and screen-reader users. Hand it back to the row's
3685
+ * Send button — next frame, once the `when` block has torn down and the button, disabled
3686
+ * while the box was open, is interactive again.
3687
+ */
3688
+ restoreDebugLogTriggerFocus() {
3689
+ requestAnimationFrame(() => {
3690
+ var _a, _b;
3691
+ if (!this.isConnected || this.debugLogComposeOpen)
3692
+ return;
3693
+ (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[part="submit-log-button"]')) === null || _b === void 0 ? void 0 : _b.focus();
3694
+ });
3695
+ }
3696
+ /**
3697
+ * After a FAILED send the controls come back but the element that started the send
3698
+ * was disabled meanwhile, so focus has already dropped to the document body. Put it
3699
+ * where the retry happens: the note when the box is open, else the row trigger.
3700
+ */
3701
+ restoreDebugLogRetryFocus() {
3702
+ if (!this.debugLogComposeOpen) {
3703
+ this.restoreDebugLogTriggerFocus();
3704
+ return;
3705
+ }
3706
+ requestAnimationFrame(() => {
3707
+ var _a, _b, _c;
3708
+ if (!this.isConnected || !this.debugLogComposeOpen)
3709
+ return;
3710
+ const el = this.debugLogNoteEl;
3711
+ (_c = ((_b = (_a = el === null || el === void 0 ? void 0 : el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('textarea')) !== null && _b !== void 0 ? _b : el)) === null || _c === void 0 ? void 0 : _c.focus();
3712
+ });
3713
+ }
3714
+ /**
3715
+ * Record the submit outcome. 'sent' flashes and falls back to idle; 'error' PERSISTS
3716
+ * until the user acts (types, reopens the box, cancels, or retries) — a failure that
3717
+ * silently reverted to the counter after four seconds left no trace it had happened.
3718
+ */
3719
+ setDebugLogSubmitResult(result) {
3720
+ clearTimeout(this.debugLogSubmitResultTimer);
3721
+ this.debugLogSubmitResultTimer = undefined;
3722
+ this.debugLogSubmitResult = result;
3723
+ if (result !== 'sent')
3724
+ return;
3725
+ this.debugLogSubmitResultTimer = setTimeout(() => {
3726
+ this.debugLogSubmitResult = null;
3727
+ this.debugLogSubmitResultTimer = undefined;
3728
+ }, this.debugLogSubmitResultFlashMs);
3729
+ }
3291
3730
  /**
3292
3731
  * The log to download (GENC-1351 §5.8). When the provider persists diagnostics,
3293
3732
  * stitch the **whole lifetime** stream (every page load) rather than just the
@@ -3896,6 +4335,9 @@ __decorate([
3896
4335
  __decorate([
3897
4336
  volatile
3898
4337
  ], FoundationAiAssistant.prototype, "busy", null);
4338
+ __decorate([
4339
+ observable
4340
+ ], FoundationAiAssistant.prototype, "timestampClock", void 0);
3899
4341
  __decorate([
3900
4342
  volatile
3901
4343
  ], FoundationAiAssistant.prototype, "effectiveChatInputDuringExecution", null);
@@ -3905,6 +4347,18 @@ __decorate([
3905
4347
  __decorate([
3906
4348
  observable
3907
4349
  ], FoundationAiAssistant.prototype, "downloadingDebugLog", void 0);
4350
+ __decorate([
4351
+ observable
4352
+ ], FoundationAiAssistant.prototype, "submittingDebugLog", void 0);
4353
+ __decorate([
4354
+ observable
4355
+ ], FoundationAiAssistant.prototype, "debugLogSubmitResult", void 0);
4356
+ __decorate([
4357
+ observable
4358
+ ], FoundationAiAssistant.prototype, "debugLogComposeOpen", void 0);
4359
+ __decorate([
4360
+ observable
4361
+ ], FoundationAiAssistant.prototype, "debugLogNote", void 0);
3908
4362
  __decorate([
3909
4363
  observable
3910
4364
  ], FoundationAiAssistant.prototype, "persistenceBroken", void 0);
@@ -3953,6 +4407,39 @@ __decorate([
3953
4407
  __decorate([
3954
4408
  volatile
3955
4409
  ], FoundationAiAssistant.prototype, "agentPickerEnabled", null);
4410
+ __decorate([
4411
+ volatile
4412
+ ], FoundationAiAssistant.prototype, "settingsAgentLogSectionVisible", null);
4413
+ __decorate([
4414
+ volatile
4415
+ ], FoundationAiAssistant.prototype, "debugLogNoteConfig", null);
4416
+ __decorate([
4417
+ volatile
4418
+ ], FoundationAiAssistant.prototype, "debugLogNoteMissing", null);
4419
+ __decorate([
4420
+ volatile
4421
+ ], FoundationAiAssistant.prototype, "debugLogSubmitLabel", null);
4422
+ __decorate([
4423
+ volatile
4424
+ ], FoundationAiAssistant.prototype, "debugLogSubmitButtonLabel", null);
4425
+ __decorate([
4426
+ volatile
4427
+ ], FoundationAiAssistant.prototype, "debugLogComposeHint", null);
4428
+ __decorate([
4429
+ volatile
4430
+ ], FoundationAiAssistant.prototype, "debugLogNoteAriaLabel", null);
4431
+ __decorate([
4432
+ volatile
4433
+ ], FoundationAiAssistant.prototype, "debugLogSubmitAnnouncement", null);
4434
+ __decorate([
4435
+ volatile
4436
+ ], FoundationAiAssistant.prototype, "debugLogRowDescription", null);
4437
+ __decorate([
4438
+ volatile
4439
+ ], FoundationAiAssistant.prototype, "debugLogSendDisabled", null);
4440
+ __decorate([
4441
+ volatile
4442
+ ], FoundationAiAssistant.prototype, "debugLogNoteCounter", null);
3956
4443
  __decorate([
3957
4444
  volatile
3958
4445
  ], FoundationAiAssistant.prototype, "pinnedAgentHint", null);