@burdenoff/fe-libs 2026.1029.3 → 2026.1029.5

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.
@@ -0,0 +1,51 @@
1
+ import { FC } from 'react';
2
+ import { AssistantModeConfig } from '../../conversation';
3
+ import { AssistantTr } from '../../i18n';
4
+ import { AssistantMode } from '../../types';
5
+ import { AssistantAutoOption } from './AssistantModeSelector';
6
+ /** Why the Mode menu cannot be used right now (linked to the trigger by `aria-describedby`). */
7
+ export type AssistantModeDisabledReason = 'replying' | 'busy';
8
+ export interface AssistantModeMenuProps {
9
+ mode: AssistantMode;
10
+ /** Role-filtered list of modes the user may pick (admin-gated modes removed). */
11
+ availableModes: AssistantModeConfig[];
12
+ onModeChange: (mode: AssistantMode) => void;
13
+ /** Offer Auto as the first option. Absent where Auto is not offered. */
14
+ auto?: AssistantAutoOption;
15
+ disabled?: boolean;
16
+ /** Why it is disabled. Ignored while enabled. Absent while disabled: `busy`. */
17
+ disabledReason?: AssistantModeDisabledReason | null;
18
+ }
19
+ /** A mode's name, exactly as its classic pill names it: the translated product label. */
20
+ export declare function assistantModeOptionName(tr: AssistantTr, modeConfig: Pick<AssistantModeConfig, 'id'> & Partial<Pick<AssistantModeConfig, 'label' | 'producesArtifacts'>>): string;
21
+ /**
22
+ * The compact presentation's Mode control (BOFF-10055, Slice 1): an APG menu
23
+ * button over the fe-libs `DropdownMenu` radio primitives. The selected option
24
+ * is always visible on the trigger, so the person always knows which mode a
25
+ * message goes to (D6).
26
+ *
27
+ * Keyboard (RECONCILIATION §D, `modeMenuKeys.ts`):
28
+ * - Enter, Space or ↓ open with focus on the checked option; ↑ on the last;
29
+ * - ↑/↓ browse and wrap, Home/End jump, and nothing but Enter, Space or a
30
+ * click CHOOSES. The classic pills commit on every arrow press (F-02);
31
+ * - Escape closes without a change and focus returns to the trigger;
32
+ * - Tab and Shift+Tab close without a change and move on to the next or
33
+ * previous focusable element. Radix keeps an open menu on Tab
34
+ * (`preventDefault`), so the items take Tab first.
35
+ *
36
+ * ★ Choosing goes through the SAME callbacks the classic pills call
37
+ * (`onModeChange`, `auto.onSelect`), once. Choosing the option already selected
38
+ * changes nothing and calls nothing.
39
+ *
40
+ * ★ Disabled is `aria-disabled`, not `disabled`: the trigger stays in the Tab
41
+ * order so a keyboard or screen-reader user reaches it and hears WHY
42
+ * (`aria-describedby`), and it cannot open.
43
+ *
44
+ * ★ Non-modal (`modal={false}`): the assistant panel is non-modal, so the page
45
+ * behind stays reachable, and nothing else is hidden from assistive technology
46
+ * while the menu is open.
47
+ *
48
+ * With one mode and no Auto there is nothing to choose: a static label, with no
49
+ * role and no Tab stop, says which mode is in use.
50
+ */
51
+ export declare const AssistantModeMenu: FC<AssistantModeMenuProps>;
@@ -0,0 +1,188 @@
1
+ import { DropdownMenu as e, DropdownMenuContent as t, DropdownMenuRadioGroup as n, DropdownMenuRadioItem as r, DropdownMenuTrigger as i } from "../../../../ui/dropdown-menu.js";
2
+ import { focusWasDropped as a } from "../../../hooks/useReturnFocus.js";
3
+ import { useAssistantTr as o } from "../../i18n.js";
4
+ import { AUTO_WRITE_MODE as s } from "../../autoRouting.js";
5
+ import { assistantModeShortDescriptionKey as ee, translateProductCopy as te } from "../../productCopy.js";
6
+ import { AUTO_MODE_OPTION_ID as c, assistantModeMeta as l } from "./AssistantModeSelector.js";
7
+ import { TAB_STOP_SELECTOR as u, modeMenuItemKey as d, modeMenuOpenIndex as ne, modeMenuTriggerKey as f, nextTabStop as re } from "./modeMenuKeys.js";
8
+ import { useCallback as p, useId as m, useRef as h, useState as g } from "react";
9
+ import { ChevronDown as ie, Wand2 as ae } from "lucide-react";
10
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
11
+ //#region src/shared/assistant/ui/composer/AssistantModeMenu.tsx
12
+ function b(e, t) {
13
+ return e(`mode.${t.id}`, t.label || l(t).fallback);
14
+ }
15
+ var oe = "[data-radix-focus-guard],[data-radix-menu-content]";
16
+ function se(e) {
17
+ if (e.disabled === !0 || e.getAttribute("type") === "hidden" || e.closest("[inert],[hidden]")) return !0;
18
+ let t = e.checkVisibility;
19
+ return typeof t == "function" ? !t.call(e) : !1;
20
+ }
21
+ var x = ({ mode: x, availableModes: S, onModeChange: C, auto: w, disabled: T = !1, disabledReason: E = null }) => {
22
+ let D = o(), O = m(), k = m(), [A, j] = g(!1);
23
+ A && T && j(!1);
24
+ let M = h(null), N = h([]), P = h("checked"), F = h(!1), I = w !== void 0, L = [...I ? [c] : [], ...S.map((e) => e.id)], R = w?.active ? c : x, z = L.indexOf(R), B = !!w?.active && x === "api-calls", V = S.find((e) => e.id === s), H = D("modeSelector.auto", "Auto"), U = D("modeSelector.autoHint", "Answers your questions, and proposes changes for you to approve"), W = S.find((e) => e.id === x), G = B && V ? b(D, V) : null, K = w?.active ? G ? `${H} · ${G}` : H : b(D, W ?? { id: x }), q = p((e) => {
25
+ if (e === R) return;
26
+ if (e === ":auto") {
27
+ w?.onSelect();
28
+ return;
29
+ }
30
+ let t = S.find((t) => t.id === e);
31
+ t && C(t.id);
32
+ }, [
33
+ w,
34
+ S,
35
+ C,
36
+ R
37
+ ]), ce = p((e) => {
38
+ e && T || (e ? F.current = !0 : P.current = "checked", j(e));
39
+ }, [T]), le = p((e) => {
40
+ let t = f(e.key);
41
+ if (t !== null) {
42
+ if (T) {
43
+ e.preventDefault();
44
+ return;
45
+ }
46
+ P.current = t, e.key === "ArrowUp" && (e.preventDefault(), F.current = !0, j(!0));
47
+ }
48
+ }, [T]), J = p((e) => {
49
+ T && e.preventDefault();
50
+ }, [T]), Y = p((e) => {
51
+ let t = M.current;
52
+ if (j(!1), !t) return;
53
+ let n = t.ownerDocument, r = Array.from(n.querySelectorAll(u)).filter((e) => !N.current.includes(e) && !e.closest(oe)), i = r.indexOf(t);
54
+ (r[i < 0 ? -1 : re(r.map((e) => ({
55
+ tabIndex: e.tabIndex,
56
+ unreachable: se(e)
57
+ })), i, e)] ?? t).focus();
58
+ }, []), X = p((e) => {
59
+ if (e.defaultPrevented) return;
60
+ let t = N.current.slice(0, L.length), n = t.findIndex((t) => t !== null && t === e.target), r = d(e.key, {
61
+ shiftKey: e.shiftKey,
62
+ index: n,
63
+ count: L.length
64
+ });
65
+ r.kind === "move" ? (e.preventDefault(), t[r.index]?.focus()) : r.kind === "tab-out" && (e.preventDefault(), Y(r.direction));
66
+ }, [L.length, Y]), ue = p((e) => {
67
+ if (!F.current || e.target !== e.currentTarget) return;
68
+ F.current = !1, e.preventDefault();
69
+ let t = ne(P.current, z, L.length);
70
+ P.current = "checked", N.current[t]?.focus();
71
+ }, [z, L.length]), de = p((e) => {
72
+ let t = M.current;
73
+ t && !a(t.ownerDocument) && e.preventDefault();
74
+ }, []), fe = p((e) => {
75
+ M.current?.focus(), q(e);
76
+ }, [q]);
77
+ if (L.length === 0) return null;
78
+ if (L.length === 1) return /* @__PURE__ */ v("span", {
79
+ className: "inline-flex h-9 items-center px-3 text-sm text-text-secondary",
80
+ "data-testid": "assistant-mode-static",
81
+ children: D("modeMenu.staticLabel", "Mode: {{mode}}", { mode: K })
82
+ });
83
+ let Z = E === "replying" ? D("modeMenu.disabledReplying", "Mode can't change while the assistant is replying. Stop the reply to change it.") : D("modeMenu.disabledBusy", "Mode can't change right now."), pe = w?.active ? ae : l({ id: x }).icon, Q = "min-h-11 items-start gap-0 py-2 pr-3", $ = (e) => (t) => {
84
+ N.current[e] = t;
85
+ };
86
+ return /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ y(e, {
87
+ open: A,
88
+ onOpenChange: ce,
89
+ modal: !1,
90
+ children: [/* @__PURE__ */ v(i, {
91
+ asChild: !0,
92
+ children: /* @__PURE__ */ y("button", {
93
+ ref: M,
94
+ type: "button",
95
+ "aria-label": D("modeMenu.trigger", "Assistant mode: {{mode}}", { mode: K }),
96
+ "aria-disabled": T || void 0,
97
+ "aria-describedby": T ? O : void 0,
98
+ title: T ? Z : void 0,
99
+ onKeyDown: le,
100
+ onPointerDown: J,
101
+ className: `inline-flex h-9 min-w-0 items-center gap-2 rounded-full pl-3 pr-2 text-sm font-medium text-text-primary transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] ${T ? "cursor-not-allowed opacity-50" : "hover:bg-bg-surface"}`,
102
+ "data-testid": "assistant-mode-selector",
103
+ children: [
104
+ /* @__PURE__ */ v(pe, {
105
+ className: "size-4 shrink-0 text-text-secondary",
106
+ "aria-hidden": "true"
107
+ }),
108
+ /* @__PURE__ */ v("span", {
109
+ className: "truncate",
110
+ children: K
111
+ }),
112
+ /* @__PURE__ */ v(ie, {
113
+ className: "size-4 shrink-0 text-text-muted",
114
+ "aria-hidden": "true"
115
+ })
116
+ ]
117
+ })
118
+ }), /* @__PURE__ */ v(t, {
119
+ side: "top",
120
+ align: "start",
121
+ sideOffset: 8,
122
+ collisionPadding: 8,
123
+ onFocus: ue,
124
+ onCloseAutoFocus: de,
125
+ onKeyDown: X,
126
+ className: "w-72 max-w-[calc(100vw-2rem)] max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto",
127
+ "data-testid": "assistant-mode-menu",
128
+ children: /* @__PURE__ */ y(n, {
129
+ value: R,
130
+ onValueChange: fe,
131
+ children: [I ? /* @__PURE__ */ v(r, {
132
+ ref: $(0),
133
+ value: c,
134
+ onKeyDown: X,
135
+ "aria-labelledby": `${k}-label-0`,
136
+ "aria-describedby": `${k}-description-0`,
137
+ className: Q,
138
+ "data-testid": "assistant-mode-auto",
139
+ "data-auto-routed": B ? s : void 0,
140
+ children: /* @__PURE__ */ y("span", {
141
+ className: "flex min-w-0 flex-col",
142
+ children: [/* @__PURE__ */ v("span", {
143
+ id: `${k}-label-0`,
144
+ className: "font-medium",
145
+ children: H
146
+ }), /* @__PURE__ */ v("span", {
147
+ id: `${k}-description-0`,
148
+ className: "text-xs text-text-secondary",
149
+ "data-testid": "assistant-mode-description",
150
+ children: U
151
+ })]
152
+ })
153
+ }) : null, S.map((e, t) => {
154
+ let n = t + +!!I, i = e.shortDescription?.trim() ? te(D, ee(e.id), e.shortDescription) : null;
155
+ return /* @__PURE__ */ v(r, {
156
+ ref: $(n),
157
+ value: e.id,
158
+ onKeyDown: X,
159
+ "aria-labelledby": `${k}-label-${n}`,
160
+ "aria-describedby": i ? `${k}-description-${n}` : void 0,
161
+ className: Q,
162
+ "data-testid": `assistant-mode-option-${e.id}`,
163
+ children: /* @__PURE__ */ y("span", {
164
+ className: "flex min-w-0 flex-col",
165
+ children: [/* @__PURE__ */ v("span", {
166
+ id: `${k}-label-${n}`,
167
+ className: "font-medium",
168
+ children: b(D, e)
169
+ }), i ? /* @__PURE__ */ v("span", {
170
+ id: `${k}-description-${n}`,
171
+ className: "text-xs text-text-secondary",
172
+ "data-testid": "assistant-mode-description",
173
+ children: i
174
+ }) : null]
175
+ })
176
+ }, e.id);
177
+ })]
178
+ })
179
+ })]
180
+ }), T ? /* @__PURE__ */ v("span", {
181
+ id: O,
182
+ className: "sr-only",
183
+ "data-testid": "assistant-mode-disabled-reason",
184
+ children: Z
185
+ }) : null] });
186
+ };
187
+ //#endregion
188
+ export { x as AssistantModeMenu, b as assistantModeOptionName };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * IME-safe Enter for the COMPACT composer (BOFF-10055, Slice 1).
3
+ *
4
+ * People typing Japanese, Chinese, Hindi, Tamil and many other languages
5
+ * compose words through an input method editor (IME). While a composition is
6
+ * open, Enter belongs to the IME: it commits the candidate the person picked.
7
+ * A composer that sends on every Enter sends a half-written message instead.
8
+ *
9
+ * Two signals, because browsers disagree:
10
+ * - `isComposing` is true on a keydown that happens inside a composition
11
+ * (Chrome, Firefox, Edge);
12
+ * - Safari fires the Enter that ENDS a composition after `compositionend`,
13
+ * with `isComposing` already false but `keyCode` 229 ("Process"), which every
14
+ * engine reports for a key the IME consumed.
15
+ *
16
+ * The same test the Avatar tab's ask box has always used
17
+ * (`ui/avatar/AvatarTabAskBox.tsx`, left untouched). The CLASSIC composer does
18
+ * not use it: classic behaviour is frozen, and changing it there needs its own
19
+ * approval (decision IME-C).
20
+ *
21
+ * Pure: no React, no DOM. Synthetic-event tests prove this logic only; real
22
+ * input methods are verified separately.
23
+ */
24
+ /** The keyCode an input method reports for a key it consumed while composing. */
25
+ export declare const IME_PROCESS_KEY_CODE = 229;
26
+ /** The parts of a keydown this check reads: a React event, or a native one. */
27
+ export interface ImeKeyEventLike {
28
+ readonly key?: string;
29
+ readonly keyCode?: number;
30
+ readonly isComposing?: boolean;
31
+ readonly nativeEvent?: {
32
+ readonly isComposing?: boolean;
33
+ readonly keyCode?: number;
34
+ };
35
+ }
36
+ /** Is this keydown a key an input method is still composing with? */
37
+ export declare function isImeComposingEnter(event: ImeKeyEventLike): boolean;
38
+ /**
39
+ * Should this keydown SEND the compact composer's message? Enter without Shift,
40
+ * and only when no input method owns the key.
41
+ */
42
+ export declare function compactEnterSends(event: ImeKeyEventLike & {
43
+ readonly shiftKey?: boolean;
44
+ }): boolean;
@@ -0,0 +1,11 @@
1
+ //#region src/shared/assistant/ui/composer/imeEnter.ts
2
+ var e = 229;
3
+ function t(e) {
4
+ let t = e.nativeEvent;
5
+ return t?.isComposing === !0 || e.isComposing === !0 ? !0 : (t?.keyCode ?? e.keyCode) === 229;
6
+ }
7
+ function n(e) {
8
+ return e.key !== "Enter" || e.shiftKey === !0 ? !1 : !t(e);
9
+ }
10
+ //#endregion
11
+ export { e as IME_PROCESS_KEY_CODE, n as compactEnterSends, t as isImeComposingEnter };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The compact Mode menu's keyboard contract (BOFF-10055, Slice 1), as pure
3
+ * decisions the component wires into the Radix menu.
4
+ *
5
+ * APG menu button with `menuitemradio` items (RECONCILIATION §D):
6
+ * - on the trigger, Enter, Space or ↓ open the menu with focus on the CHECKED
7
+ * item; ↑ opens it with focus on the LAST item;
8
+ * - inside, ↑/↓ browse and wrap, Home/End jump, and NONE of them changes the
9
+ * mode. This fixes F-02: the classic pills commit on every arrow press;
10
+ * - Enter or Space chooses the focused option, once;
11
+ * - Escape closes without a change and returns focus to the trigger;
12
+ * - Tab and Shift+Tab close without a change and CONTINUE sequential
13
+ * navigation to the next or previous focusable element. They do not return
14
+ * to the trigger. Radix (2.1.16 and 2.1.24) calls `preventDefault()` on Tab
15
+ * inside menu content and keeps the menu open, so the component handles Tab
16
+ * itself, before Radix sees it.
17
+ *
18
+ * Pure: no React, no DOM.
19
+ */
20
+ /** Where focus goes when the menu opens. */
21
+ export type ModeMenuOpenFocus = 'checked' | 'last';
22
+ /** What a key on the closed trigger does: open (and where focus lands), or nothing. */
23
+ export declare function modeMenuTriggerKey(key: string): ModeMenuOpenFocus | null;
24
+ /** The item index focus starts on: the checked item, else the first; or the last. */
25
+ export declare function modeMenuOpenIndex(focus: ModeMenuOpenFocus, checkedIndex: number, count: number): number;
26
+ /** What a key inside the open menu does. */
27
+ export type ModeMenuItemKeyAction =
28
+ /** Move focus to `index`; the mode does not change. */
29
+ {
30
+ kind: 'move';
31
+ index: number;
32
+ }
33
+ /** Choose the focused option (once). */
34
+ | {
35
+ kind: 'commit';
36
+ }
37
+ /** Close; focus returns to the trigger. */
38
+ | {
39
+ kind: 'close';
40
+ }
41
+ /** Close; focus continues to the next (`1`) or previous (`-1`) focusable element. */
42
+ | {
43
+ kind: 'tab-out';
44
+ direction: 1 | -1;
45
+ }
46
+ /** Not a key the menu handles. */
47
+ | {
48
+ kind: 'none';
49
+ };
50
+ /** The menu's decision for one key, given the focused item and how many there are. */
51
+ export declare function modeMenuItemKey(key: string, options: {
52
+ shiftKey?: boolean;
53
+ index: number;
54
+ count: number;
55
+ }): ModeMenuItemKeyAction;
56
+ /**
57
+ * Selector for elements sequential (Tab) navigation can reach, before the
58
+ * `tabindex`, `disabled`, `inert` and visibility checks `nextTabStop` applies.
59
+ */
60
+ export declare const TAB_STOP_SELECTOR: string;
61
+ /** What `nextTabStop` needs to know about one candidate. */
62
+ export interface TabStopCandidate {
63
+ readonly tabIndex: number;
64
+ /** Disabled, inside an inert subtree, hidden, or a `type="hidden"` input. */
65
+ readonly unreachable: boolean;
66
+ }
67
+ /**
68
+ * The index, in DOCUMENT ORDER, of the element Tab (`direction` 1) or
69
+ * Shift+Tab (`-1`) moves to from the element at `fromIndex`, or -1 when there
70
+ * is none (the browser then leaves the page, which this code does not imitate).
71
+ *
72
+ * Only `tabIndex >= 0` and reachable candidates count. Positive tabindex
73
+ * ordering is not modelled: the assistant uses none, and document order is
74
+ * what such pages already get.
75
+ */
76
+ export declare function nextTabStop(candidates: readonly TabStopCandidate[], fromIndex: number, direction: 1 | -1): number;
@@ -0,0 +1,61 @@
1
+ //#region src/shared/assistant/ui/composer/modeMenuKeys.ts
2
+ function e(e) {
3
+ switch (e) {
4
+ case "Enter":
5
+ case " ":
6
+ case "ArrowDown": return "checked";
7
+ case "ArrowUp": return "last";
8
+ default: return null;
9
+ }
10
+ }
11
+ function t(e, t, n) {
12
+ return n <= 0 ? -1 : e === "last" ? n - 1 : t >= 0 && t < n ? t : 0;
13
+ }
14
+ function n(e, t) {
15
+ let { index: n, count: r } = t, i = r - 1;
16
+ switch (e) {
17
+ case "ArrowDown": return {
18
+ kind: "move",
19
+ index: n >= i ? 0 : n + 1
20
+ };
21
+ case "ArrowUp": return {
22
+ kind: "move",
23
+ index: n <= 0 ? i : n - 1
24
+ };
25
+ case "Home": return {
26
+ kind: "move",
27
+ index: 0
28
+ };
29
+ case "End": return {
30
+ kind: "move",
31
+ index: i
32
+ };
33
+ case "Enter":
34
+ case " ": return { kind: "commit" };
35
+ case "Escape": return { kind: "close" };
36
+ case "Tab": return {
37
+ kind: "tab-out",
38
+ direction: t.shiftKey === !0 ? -1 : 1
39
+ };
40
+ default: return { kind: "none" };
41
+ }
42
+ }
43
+ var r = [
44
+ "a[href]",
45
+ "button",
46
+ "input",
47
+ "select",
48
+ "textarea",
49
+ "summary",
50
+ "[tabindex]",
51
+ "[contenteditable=\"true\"]"
52
+ ].join(",");
53
+ function i(e, t, n) {
54
+ for (let r = t + n; r >= 0 && r < e.length; r += n) {
55
+ let t = e[r];
56
+ if (t && t.tabIndex >= 0 && !t.unreachable) return r;
57
+ }
58
+ return -1;
59
+ }
60
+ //#endregion
61
+ export { r as TAB_STOP_SELECTOR, n as modeMenuItemKey, t as modeMenuOpenIndex, e as modeMenuTriggerKey, i as nextTabStop };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What switching the assistant's mode would do, decided BEFORE it is done
3
+ * (BOFF-10055, Slice 1: the D7 confirmation merged with first-time write
4
+ * consent). COMPACT presentation only; classic switches as it always has.
5
+ *
6
+ * A switch can cost the person something they cannot see from the menu:
7
+ * - changes waiting for their approval are WITHDRAWN (`mode_switched`);
8
+ * - a conversation with history is left in History and the new mode CONTINUES
9
+ * IN ANOTHER CONVERSATION.
10
+ * When either is true, or the target is a write-capable mode this profile has
11
+ * not consented to yet, ONE dialog asks first. Never two: consent and the
12
+ * switch confirmation are one decision.
13
+ *
14
+ * ★★ The predicates are the ones the switch itself uses, so the dialog can
15
+ * never promise something the switch then does differently
16
+ * (`AssistantChatArea.tsx`, guarded by `modeSwitchPlan.test.ts`):
17
+ * - a MODE pick (`performModeChange`): withdraw when the open conversation's
18
+ * mode differs from the target; move when it also has history
19
+ * (`conversationHasHistory`);
20
+ * - consent (`handleModeChange`): the target is `writeCapable` and not in
21
+ * `acknowledgedWriteModes`;
22
+ * - AUTO (`handleSelectAuto`): nothing happens to a conversation Auto already
23
+ * routes, or to one without history (it is adopted in place); one with
24
+ * history whose mode the person chose is withdrawn from and left. Auto never
25
+ * asks for consent here: a write is consented to when Auto routes a send.
26
+ *
27
+ * The draft is never lost by a switch (the composer is not remounted), so it is
28
+ * not a reason to ask; when the dialog shows anyway, it says the draft stays.
29
+ *
30
+ * Pure: no React, no store.
31
+ */
32
+ /** The open conversation, as the switch reads it; `null` when none is open. */
33
+ export interface ModeSwitchConversation {
34
+ /** The conversation's own mode. */
35
+ readonly mode: string;
36
+ /** `conversationHasHistory(conversation)`. */
37
+ readonly hasHistory: boolean;
38
+ /** `conversation.routedBy === 'auto'`. */
39
+ readonly routedByAuto: boolean;
40
+ }
41
+ export type ModeSwitchTarget =
42
+ /** A mode picked by hand. */
43
+ {
44
+ readonly kind: 'mode';
45
+ readonly mode: string;
46
+ readonly consentRequired: boolean;
47
+ }
48
+ /** Auto (BOFF-7800). */
49
+ | {
50
+ readonly kind: 'auto';
51
+ };
52
+ export interface ModeSwitchInput {
53
+ readonly target: ModeSwitchTarget;
54
+ readonly conversation: ModeSwitchConversation | null;
55
+ /** Changes of the open conversation waiting for approval (`liveFor(id).count`). */
56
+ readonly waitingCount: number;
57
+ /** The composer holds text or attachments. */
58
+ readonly hasDraft: boolean;
59
+ }
60
+ export interface ModeSwitchPlan {
61
+ /** Ask before switching: consent applies, or something would be withdrawn or left. */
62
+ readonly needsDialog: boolean;
63
+ /** First-time write consent for the target mode. */
64
+ readonly consent: boolean;
65
+ /** Waiting changes the switch withdraws. */
66
+ readonly withdrawnCount: number;
67
+ /** The open conversation stays in History and the target continues in another one. */
68
+ readonly movesToNewConversation: boolean;
69
+ /** Say that the typed draft stays (only shown alongside a withdraw or a move). */
70
+ readonly draftKept: boolean;
71
+ }
72
+ /** What a switch to `input.target` would do, and whether to ask first. */
73
+ export declare function planModeSwitch(input: ModeSwitchInput): ModeSwitchPlan;
@@ -0,0 +1,25 @@
1
+ //#region src/shared/assistant/ui/composer/modeSwitchPlan.ts
2
+ var e = {
3
+ needsDialog: !1,
4
+ consent: !1,
5
+ withdrawnCount: 0,
6
+ movesToNewConversation: !1,
7
+ draftKept: !1
8
+ };
9
+ function t(t) {
10
+ let { target: n, conversation: r } = t, i = Math.max(0, Math.floor(t.waitingCount)), a, o, s = !1;
11
+ if (n.kind === "mode") {
12
+ let e = r !== null && r.mode !== n.mode;
13
+ o = e, a = e && r.hasHistory, s = n.consentRequired;
14
+ } else a = r !== null && !r.routedByAuto && r.hasHistory, o = a;
15
+ let c = o ? i : 0, l = c > 0 || a;
16
+ return !l && !s ? e : {
17
+ needsDialog: !0,
18
+ consent: s,
19
+ withdrawnCount: c,
20
+ movesToNewConversation: a,
21
+ draftKept: l && t.hasDraft
22
+ };
23
+ }
24
+ //#endregion
25
+ export { t as planModeSwitch };