@burdenoff/fe-libs 2026.1029.3 → 2026.1029.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shared/assistant/conversation.d.ts +10 -0
- package/dist/shared/assistant/i18nManifest.d.ts +15 -2
- package/dist/shared/assistant/i18nManifest.js +12 -0
- package/dist/shared/assistant/index.d.ts +4 -0
- package/dist/shared/assistant/productCopy.d.ts +8 -0
- package/dist/shared/assistant/productCopy.js +8 -5
- package/dist/shared/assistant/ui/AIAssistantPanel.d.ts +9 -0
- package/dist/shared/assistant/ui/AIAssistantPanel.js +70 -69
- package/dist/shared/assistant/ui/AssistantApiCallsConsentDialog.d.ts +23 -0
- package/dist/shared/assistant/ui/AssistantApiCallsConsentDialog.js +72 -18
- package/dist/shared/assistant/ui/AssistantChatArea.d.ts +6 -0
- package/dist/shared/assistant/ui/AssistantChatArea.js +1176 -1065
- package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +27 -0
- package/dist/shared/assistant/ui/composer/AssistantComposer.js +350 -206
- package/dist/shared/assistant/ui/composer/AssistantModeMenu.d.ts +51 -0
- package/dist/shared/assistant/ui/composer/AssistantModeMenu.js +188 -0
- package/dist/shared/assistant/ui/composer/imeEnter.d.ts +44 -0
- package/dist/shared/assistant/ui/composer/imeEnter.js +11 -0
- package/dist/shared/assistant/ui/composer/modeMenuKeys.d.ts +76 -0
- package/dist/shared/assistant/ui/composer/modeMenuKeys.js +61 -0
- package/dist/shared/assistant/ui/composer/modeSwitchPlan.d.ts +73 -0
- package/dist/shared/assistant/ui/composer/modeSwitchPlan.js +25 -0
- package/dist/shared-assistant.js +241 -237
- package/package.json +1 -1
|
@@ -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 };
|