@burdenoff/fe-libs 2026.1029.8 → 2026.1029.10

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 (39) hide show
  1. package/dist/shared/assistant/i18nManifest.d.ts +27 -0
  2. package/dist/shared/assistant/i18nManifest.js +27 -0
  3. package/dist/shared/assistant/index.d.ts +9 -0
  4. package/dist/shared/assistant/speech/speechFeed.d.ts +4 -3
  5. package/dist/shared/assistant/speech/speechFeed.js +3 -2
  6. package/dist/shared/assistant/ui/AIAssistantPanel.d.ts +11 -0
  7. package/dist/shared/assistant/ui/AIAssistantPanel.js +233 -127
  8. package/dist/shared/assistant/ui/AssistantChatArea.d.ts +13 -1
  9. package/dist/shared/assistant/ui/AssistantChatArea.js +1632 -1605
  10. package/dist/shared/assistant/ui/AssistantConversationList.d.ts +17 -1
  11. package/dist/shared/assistant/ui/AssistantConversationList.js +304 -274
  12. package/dist/shared/assistant/ui/AssistantOverlay.d.ts +11 -0
  13. package/dist/shared/assistant/ui/AssistantOverlay.js +106 -90
  14. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +140 -137
  15. package/dist/shared/assistant/ui/closeGuard.d.ts +21 -0
  16. package/dist/shared/assistant/ui/closeGuard.js +8 -0
  17. package/dist/shared/assistant/ui/compact/AssistantCompactHeader.d.ts +38 -0
  18. package/dist/shared/assistant/ui/compact/AssistantCompactHeader.js +89 -0
  19. package/dist/shared/assistant/ui/compact/AssistantConversationMenu.d.ts +29 -0
  20. package/dist/shared/assistant/ui/compact/AssistantConversationMenu.js +87 -0
  21. package/dist/shared/assistant/ui/compact/AssistantDiscardDraftDialog.d.ts +19 -0
  22. package/dist/shared/assistant/ui/compact/AssistantDiscardDraftDialog.js +38 -0
  23. package/dist/shared/assistant/ui/compact/AssistantMenuView.d.ts +42 -0
  24. package/dist/shared/assistant/ui/compact/AssistantMenuView.js +285 -0
  25. package/dist/shared/assistant/ui/compact/AssistantVoiceLanguageSettings.d.ts +24 -0
  26. package/dist/shared/assistant/ui/compact/AssistantVoiceLanguageSettings.js +68 -0
  27. package/dist/shared/assistant/ui/compact/AssistantWaitingChangeRow.d.ts +21 -0
  28. package/dist/shared/assistant/ui/compact/AssistantWaitingChangeRow.js +34 -0
  29. package/dist/shared/assistant/ui/compact/compactChrome.d.ts +75 -0
  30. package/dist/shared/assistant/ui/compact/compactChrome.js +39 -0
  31. package/dist/shared/assistant/ui/compact/waitingChange.d.ts +49 -0
  32. package/dist/shared/assistant/ui/compact/waitingChange.js +12 -0
  33. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +19 -18
  34. package/dist/shared/assistant/ui/composer/AssistantComposer.js +261 -224
  35. package/dist/shared/assistant/ui/useVoiceSession.js +13 -8
  36. package/dist/shared/assistant/useAssistantKeyboardShortcuts.d.ts +16 -2
  37. package/dist/shared/assistant/useAssistantKeyboardShortcuts.js +28 -16
  38. package/dist/shared-assistant.js +42 -33
  39. package/package.json +1 -1
@@ -0,0 +1,68 @@
1
+ import { useAssistantTr as e } from "../../i18n.js";
2
+ import { useAssistantSpeech as t } from "../AssistantSpeechProvider.js";
3
+ import { AssistantLanguageMenu as n } from "../AssistantLanguageMenu.js";
4
+ import { AssistantVoiceSelector as r } from "../composer/AssistantVoiceSelector.js";
5
+ import { AssistantVoiceGraceSelector as i } from "../composer/AssistantVoiceGraceSelector.js";
6
+ import { AssistantHandsFreeInterruptToggle as a } from "../composer/AssistantHandsFreeInterruptToggle.js";
7
+ import { useId as o } from "react";
8
+ import { Play as s, Volume2 as c } from "lucide-react";
9
+ import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
10
+ //#region src/shared/assistant/ui/compact/AssistantVoiceLanguageSettings.tsx
11
+ var f = () => {
12
+ let f = e(), p = o(), { isSupported: m, isBlocked: h, disabledReason: g, autoSpeakEnabled: _, setAutoSpeak: v, stop: y, previewVoice: b, activeVoiceName: x } = t(), S = h && !_;
13
+ return /* @__PURE__ */ d("div", {
14
+ className: "py-1 [&_label]:min-h-11 [&_label]:text-sm",
15
+ "data-testid": "assistant-menu-voice-language",
16
+ children: [
17
+ m ? /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ d("label", {
18
+ className: "flex items-center gap-2.5 px-4 py-2 text-text-primary focus-within:ring-2 focus-within:ring-inset focus-within:ring-[var(--color-focus-ring)]",
19
+ "data-testid": "assistant-auto-speak",
20
+ children: [
21
+ /* @__PURE__ */ u(c, {
22
+ className: "size-4 flex-shrink-0 text-text-secondary",
23
+ "aria-hidden": "true"
24
+ }),
25
+ /* @__PURE__ */ u("span", {
26
+ className: "flex-1",
27
+ children: f("menuView.readAloud", "Read replies aloud")
28
+ }),
29
+ /* @__PURE__ */ u("input", {
30
+ type: "checkbox",
31
+ checked: _,
32
+ "aria-disabled": S ? "true" : void 0,
33
+ "aria-describedby": S && g ? p : void 0,
34
+ onChange: () => {
35
+ S || (_ && y(), v(!_));
36
+ },
37
+ className: "size-4 accent-current aria-disabled:cursor-default aria-disabled:opacity-60",
38
+ "data-testid": "assistant-auto-speak-checkbox"
39
+ })
40
+ ]
41
+ }), S && g ? /* @__PURE__ */ u("p", {
42
+ id: p,
43
+ className: "px-4 pb-2 text-xs text-text-muted",
44
+ children: g
45
+ }) : null] }) : null,
46
+ /* @__PURE__ */ u(n, {}),
47
+ /* @__PURE__ */ d("div", {
48
+ className: "flex items-center gap-1 px-3 py-1.5",
49
+ "data-testid": "assistant-menu-voice",
50
+ children: [/* @__PURE__ */ u(r, {}), x ? /* @__PURE__ */ d("button", {
51
+ type: "button",
52
+ onClick: () => b(x),
53
+ className: "flex min-h-9 items-center gap-1 rounded-md px-2 text-xs text-text-muted transition-colors hover:bg-bg-elevated hover:text-text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
54
+ "aria-label": f("voice.preview", "Hear this voice"),
55
+ "data-testid": "assistant-voice-preview",
56
+ children: [/* @__PURE__ */ u(s, {
57
+ className: "size-3",
58
+ "aria-hidden": "true"
59
+ }), f("voice.previewShort", "Preview")]
60
+ }) : null]
61
+ }),
62
+ /* @__PURE__ */ u(i, {}),
63
+ /* @__PURE__ */ u(a, {})
64
+ ]
65
+ });
66
+ };
67
+ //#endregion
68
+ export { f as AssistantVoiceLanguageSettings };
@@ -0,0 +1,21 @@
1
+ import { FC } from 'react';
2
+ export interface AssistantWaitingChangeRowProps {
3
+ /** How many changes wait for the person's approval in the open conversation. */
4
+ count: number;
5
+ /** Scroll to the first waiting change's card and focus it. */
6
+ onReview: () => void;
7
+ }
8
+ /**
9
+ * The pinned "change waiting" row above the composer (N6, 2026-10-29), in the
10
+ * compact presentation only.
11
+ *
12
+ * ★ A pointer to the EXISTING approval card, never a second way to approve: it
13
+ * has no Approve and no Decline, only Review, which scrolls to the card and
14
+ * focuses it. The card stays the one place a change is confirmed, by a button
15
+ * press (`writeProposals.ts`: voice and typed "yes" never approve).
16
+ *
17
+ * The cards sit inside the message list, so a person who scrolled up never saw
18
+ * a new one arrive; this row is always on screen while any change waits. Its
19
+ * text is a polite status, so the arrival is announced without taking focus.
20
+ */
21
+ export declare const AssistantWaitingChangeRow: FC<AssistantWaitingChangeRowProps>;
@@ -0,0 +1,34 @@
1
+ import { useAssistantTr as e } from "../../i18n.js";
2
+ import { ShieldAlert as t } from "lucide-react";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ //#region src/shared/assistant/ui/compact/AssistantWaitingChangeRow.tsx
5
+ var i = ({ count: i, onReview: a }) => {
6
+ let o = e();
7
+ return /* @__PURE__ */ r("div", {
8
+ className: "mb-2 flex min-h-11 items-center gap-2 rounded-xl border border-status-warning bg-status-warning/10 px-3 py-1",
9
+ "data-testid": "assistant-waiting-change",
10
+ children: [
11
+ /* @__PURE__ */ n(t, {
12
+ className: "size-4 shrink-0 text-status-warning",
13
+ "aria-hidden": "true"
14
+ }),
15
+ /* @__PURE__ */ n("p", {
16
+ role: "status",
17
+ "aria-live": "polite",
18
+ className: "min-w-0 flex-1 text-sm text-text-primary",
19
+ "data-testid": "assistant-waiting-change-status",
20
+ children: o("waitingChange.status", "{{count}} change(s) waiting for your approval", { count: i })
21
+ }),
22
+ /* @__PURE__ */ n("button", {
23
+ type: "button",
24
+ onClick: a,
25
+ className: "min-h-9 shrink-0 rounded-button px-3 text-sm font-medium text-action-primary-bg transition-colors hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
26
+ "aria-label": o("waitingChange.reviewLabel", "Review the change waiting for your approval"),
27
+ "data-testid": "assistant-waiting-change-review",
28
+ children: o("waitingChange.review", "Review")
29
+ })
30
+ ]
31
+ });
32
+ };
33
+ //#endregion
34
+ export { i as AssistantWaitingChangeRow };
@@ -0,0 +1,75 @@
1
+ import { Conversation } from '../../conversation';
2
+ /**
3
+ * Expand/Restore is offered only from this viewport width up (N2, 2026-10-29).
4
+ *
5
+ * At 1536 px the page keeps at least 520 px beside the default sidebar once the
6
+ * assistant is 760 px wide (DESIGN-DELTA A2); at 1440 it would keep 424 px, so
7
+ * the control is not offered there. A viewport that is exactly this wide IS
8
+ * offered it: the rule is "at least".
9
+ */
10
+ export declare const COMPACT_EXPAND_MIN_VIEWPORT_PX = 1536;
11
+ /** Is Expand/Restore offered at this viewport width? */
12
+ export declare function compactExpandOffered(viewportWidthPx: number): boolean;
13
+ /**
14
+ * Is Expand/Restore offered at the viewport's CURRENT width? Follows a resize.
15
+ * Never offered without a window (a server render): the control appears only
16
+ * where a width was actually read.
17
+ *
18
+ * ★ `enabled` is false for the classic presentation, which then listens to
19
+ * nothing and reads nothing: the hook has to be called either way.
20
+ */
21
+ export declare function useCompactExpandOffered(enabled: boolean): boolean;
22
+ /** The ☰ view's screens: its own list, and the two sub-views opened from it. */
23
+ export type MenuViewName = 'root' | 'voice-language' | 'history';
24
+ /**
25
+ * What Escape does inside the ☰ view: a sub-view goes back to the list, and the
26
+ * list closes the view. One key press, one layer (LA-06).
27
+ */
28
+ export declare function menuViewEscape(view: MenuViewName): 'back' | 'close';
29
+ /** The parts of the focused element `fieldOwnsEscape` reads. */
30
+ export interface EscapeFieldLike {
31
+ readonly tagName?: string;
32
+ readonly value?: unknown;
33
+ getAttribute(name: string): string | null;
34
+ }
35
+ /**
36
+ * Does the focused element handle Escape ITSELF, so the ☰ view must not also
37
+ * step back? Inside History: the rename box (Escape cancels the rename) and the
38
+ * search box WHEN IT HOLDS TEXT (Escape clears it). An empty search box, a
39
+ * filter radio or a button leave Escape to the layer.
40
+ *
41
+ * ★ The ☰ view's `EscapeLayer` runs in the document CAPTURE phase, before the
42
+ * field's own React handler, so without this one press did both: cancelled the
43
+ * rename or cleared the search AND dropped back to the ☰ list.
44
+ */
45
+ export declare function fieldOwnsEscape(element: EscapeFieldLike | null): boolean;
46
+ /** What the composer reports about its draft (`AssistantComposerDraft`, the part N1 reads). */
47
+ export interface UnsentDraft {
48
+ readonly text: string;
49
+ readonly hasAttachments: boolean;
50
+ }
51
+ /**
52
+ * Is there something typed or attached that has not been sent? Whitespace alone
53
+ * is not a draft: it is the composer's own `hasDraft` rule, so Close does not
54
+ * ask about a box that looks empty.
55
+ */
56
+ export declare function draftIsUnsent(draft: UnsentDraft): boolean;
57
+ /**
58
+ * What a request to close the panel does (N1): close, or ask first.
59
+ *
60
+ * `confirmed` is true while the person's "Discard and close" is running, so the
61
+ * close that follows it is not asked about again.
62
+ */
63
+ export declare function closeRequestAction(args: {
64
+ compact: boolean;
65
+ unsent: boolean;
66
+ confirmed: boolean;
67
+ }): 'close' | 'ask';
68
+ /** How many conversations "Recent" lists. */
69
+ export declare const COMPACT_RECENT_LIMIT = 5;
70
+ /**
71
+ * The conversations "Recent" lists: the newest first, by last activity, with
72
+ * nothing in them left out (an empty draft is not something to go back to).
73
+ * The open conversation stays in the list, marked by the caller.
74
+ */
75
+ export declare function recentConversations(conversations: readonly Conversation[], limit?: number): Conversation[];
@@ -0,0 +1,39 @@
1
+ import { useSyncExternalStore as e } from "react";
2
+ //#region src/shared/assistant/ui/compact/compactChrome.ts
3
+ var t = 1536;
4
+ function n(e) {
5
+ return e >= t;
6
+ }
7
+ function r(e) {
8
+ return typeof window > "u" ? () => {} : (window.addEventListener("resize", e), () => window.removeEventListener("resize", e));
9
+ }
10
+ var i = () => () => {}, a = () => typeof window < "u" && n(window.innerWidth), o = () => !1;
11
+ function s(t) {
12
+ return e(t ? r : i, t ? a : o, o);
13
+ }
14
+ function c(e) {
15
+ return e === "root" ? "close" : "back";
16
+ }
17
+ function l(e) {
18
+ if (!e || e.tagName !== "INPUT") return !1;
19
+ let t = e.getAttribute("type");
20
+ return t !== null && t !== "text" && t !== "search" ? !1 : e.getAttribute("role") === "searchbox" ? typeof e.value == "string" && e.value !== "" : !0;
21
+ }
22
+ function u(e) {
23
+ return e.text.trim() !== "" || e.hasAttachments;
24
+ }
25
+ function d(e) {
26
+ return e.compact && e.unsent && !e.confirmed ? "ask" : "close";
27
+ }
28
+ var f = 5;
29
+ function p(e, t = 5) {
30
+ return e.filter((e) => e.messages.length > 0).map((e, t) => ({
31
+ conversation: e,
32
+ index: t
33
+ })).sort((e, t) => {
34
+ let n = Date.parse(t.conversation.updatedAt) - Date.parse(e.conversation.updatedAt);
35
+ return Number.isNaN(n) || n === 0 ? e.index - t.index : n;
36
+ }).slice(0, t).map((e) => e.conversation);
37
+ }
38
+ //#endregion
39
+ export { t as COMPACT_EXPAND_MIN_VIEWPORT_PX, f as COMPACT_RECENT_LIMIT, d as closeRequestAction, n as compactExpandOffered, u as draftIsUnsent, l as fieldOwnsEscape, c as menuViewEscape, p as recentConversations, s as useCompactExpandOffered };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The pure half of the compact "change waiting" row (N6, BOFF-10177): how many
3
+ * changes wait for the person's approval, and how Review reaches the card.
4
+ *
5
+ * ★ The row is a POINTER to the existing approval card and nothing else. These
6
+ * two functions count and find; neither approves, declines or withdraws.
7
+ */
8
+ /** What this reads of one proposal entry (`WriteProposalEntry`). */
9
+ export interface WaitingChangeEntry {
10
+ readonly session: {
11
+ readonly conversationId: string;
12
+ readonly sandboxId: string;
13
+ };
14
+ readonly view: {
15
+ readonly status: string;
16
+ };
17
+ }
18
+ /**
19
+ * How many changes wait in the open conversation. The rule of
20
+ * `writeProposalPanel.liveFor` (pending, and not in a replaced sandbox), read
21
+ * from the entries so it can be recomputed on every arrival and decision.
22
+ *
23
+ * ★ Only changes with a card ON THE PAGE are counted (`cardProposalIds`: the
24
+ * proposals the typed thread's replies reference). The row points at a card; a
25
+ * change a SPOKEN turn left behind has none once the voice session ends (the
26
+ * thread omits spoken turns and the transcript draws no cards), so counting it
27
+ * would show a Review that finds nothing. Omitted, every pending change counts.
28
+ */
29
+ export declare function countWaitingChanges(entries: Readonly<Record<string, WaitingChangeEntry>>, conversationId: string | null, retiredSandboxes: ReadonlySet<string>, cardProposalIds?: ReadonlySet<string>): number;
30
+ /** A card waiting for a decision: the existing card's own markers. */
31
+ export declare const WAITING_CARD_SELECTOR = "[data-testid=\"assistant-write-proposal\"][data-status=\"pending\"]";
32
+ /** The slice of an element Review uses. */
33
+ interface ReviewableCard {
34
+ closest(selector: string): unknown;
35
+ hasAttribute(name: string): boolean;
36
+ setAttribute(name: string, value: string): void;
37
+ scrollIntoView(options?: ScrollIntoViewOptions): void;
38
+ focus(options?: FocusOptions): void;
39
+ }
40
+ /**
41
+ * Scroll to the first waiting card under `root` and focus it. A replaced
42
+ * sandbox's card is inert and is skipped. The card is a `group` that cannot take
43
+ * focus by itself, so it is made a programmatic focus target first
44
+ * (`tabindex="-1"`, which adds no Tab stop). Returns whether a card was found.
45
+ */
46
+ export declare function focusFirstWaitingCard(root: {
47
+ querySelectorAll(selector: string): ArrayLike<ReviewableCard>;
48
+ } | null): boolean;
49
+ export {};
@@ -0,0 +1,12 @@
1
+ //#region src/shared/assistant/ui/compact/waitingChange.ts
2
+ function e(e, t, n, r) {
3
+ return t === null ? 0 : Object.entries(e).filter(([e, i]) => i.session.conversationId === t && i.view.status === "pending" && !n.has(i.session.sandboxId) && (r === void 0 || r.has(e))).length;
4
+ }
5
+ var t = "[data-testid=\"assistant-write-proposal\"][data-status=\"pending\"]";
6
+ function n(e) {
7
+ if (!e) return !1;
8
+ let n = Array.from(e.querySelectorAll(t)).find((e) => !e.closest("[inert]"));
9
+ return n ? (n.scrollIntoView({ block: "center" }), n.hasAttribute("tabindex") || n.setAttribute("tabindex", "-1"), n.focus({ preventScroll: !0 }), !0) : !1;
10
+ }
11
+ //#endregion
12
+ export { t as WAITING_CARD_SELECTOR, e as countWaitingChanges, n as focusFirstWaitingCard };
@@ -1,4 +1,4 @@
1
- import { FC } from 'react';
1
+ import { FC, ReactNode, Ref } from 'react';
2
2
  import { AssistantModeConfig } from '../../conversation';
3
3
  import { AssistantMode } from '../../types';
4
4
  import { AssistantAttachment } from '../../attachments';
@@ -95,8 +95,26 @@ interface Props {
95
95
  * none was running; only a stopped turn is announced.
96
96
  */
97
97
  onStop?: () => string | null;
98
+ /**
99
+ * The id of the reply in flight (what `onStop` returns for it), or null. Lets
100
+ * Stop tell "the reply I stopped is still ending" from "another reply has
101
+ * begun": a parked send is replayed in the stopped turn's own `finally`, so
102
+ * `turnInFlight` can stay true across the two. Absent: Stop goes by
103
+ * `turnInFlight` alone.
104
+ */
105
+ turnId?: string | null;
98
106
  /** Why the Mode menu is disabled; absent while it is disabled: `busy`. */
99
107
  modeDisabledReason?: AssistantModeDisabledReason | null;
108
+ /**
109
+ * The message box, for the panel's `/` shortcut (BOFF-10177, compact only).
110
+ * Classic never reads it: its box is exactly as before.
111
+ */
112
+ inputRef?: Ref<HTMLTextAreaElement>;
113
+ /**
114
+ * Compact only (BOFF-10177, N6): what sits pinned just above the card, the
115
+ * "change waiting" row. Classic never draws it.
116
+ */
117
+ aboveCard?: ReactNode;
100
118
  }
101
119
  /** A local draft snapshot; no files are uploaded by observing it. */
102
120
  export interface AssistantComposerDraft {
@@ -109,22 +127,5 @@ export interface AssistantComposerVoiceState {
109
127
  readonly committing: boolean;
110
128
  readonly dictating: boolean;
111
129
  }
112
- /**
113
- * AssistantComposer — the v2 input surface (per the ai-assistant-v2 mockups).
114
- *
115
- * A rounded card with, on the input row: a leading circular "+" attach control,
116
- * an auto-growing textarea, a microphone, and a prominent filled forest-green
117
- * circular send button. BELOW the input sit the mode pills ("Ask" active-green /
118
- * "Take action" outline); ABOVE it, chips for any pending attachments.
119
- *
120
- * Multimodal input (Workstreams 2–3) arrives through three converging paths —
121
- * the attach button, drag-and-drop onto the card, and paste (a screenshot from
122
- * the clipboard is the single most common way a user shares one) — plus voice
123
- * dictation, which appends recognised speech to the draft so it can still be
124
- * edited before sending.
125
- *
126
- * Fully controlled (React state for the text) with auto-grow via a layout
127
- * effect. Enter sends, Shift+Enter inserts a newline.
128
- */
129
130
  export declare const AssistantComposer: FC<Props>;
130
131
  export {};