@runtypelabs/persona 4.25.0 → 4.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
- package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +14 -0
- package/dist/animations-extra.d.ts +14 -0
- package/dist/approval-ui-QAKWNFNM.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +180 -8
- package/dist/approval-ui.d.ts +180 -8
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
- package/dist/artifacts-ui.cjs +4 -4
- package/dist/artifacts-ui.d.cts +180 -8
- package/dist/artifacts-ui.d.ts +180 -8
- package/dist/artifacts-ui.js +5 -5
- package/dist/chunk-2ANTRKTV.js +1 -0
- package/dist/chunk-4HPTPUC6.js +1 -0
- package/dist/chunk-6V3MNEKB.js +1 -0
- package/dist/chunk-HGUCGM3K.js +9 -0
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-J4J3Y2P2.js +1 -0
- package/dist/chunk-K6HKIP5K.js +1 -0
- package/dist/chunk-K7NA7G7J.js +1 -0
- package/dist/chunk-MXUTS3LB.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-QPPJUSZH.js +1 -0
- package/dist/chunk-QRKGQPU7.js +1 -0
- package/dist/chunk-S6PBWE75.js +1 -0
- package/dist/chunk-T7QM7YMF.js +1 -0
- package/dist/chunk-TR2H475E.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/chunk-ZC66NAIR.js +1 -0
- package/dist/client-history-YPEG45Q4.js +1 -0
- package/dist/client-history.js +1 -0
- package/dist/client-stream-42FOI3EC.js +4 -0
- package/dist/client-stream.js +5 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions-E7HJYWO6.js +161 -0
- package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
- package/dist/context-mentions.cjs +7 -7
- package/dist/context-mentions.d.cts +180 -8
- package/dist/context-mentions.d.ts +180 -8
- package/dist/context-mentions.js +8 -8
- package/dist/event-stream-view-FOIJHFQO.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +183 -31
- package/dist/event-stream-view.d.ts +183 -31
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +221 -667
- package/dist/forms-ui.d.ts +221 -667
- package/dist/history-shell-QDQ44PSE.js +3 -0
- package/dist/history-shell.js +3 -0
- package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
- package/dist/history-view.cjs +2 -2
- package/dist/history-view.d.cts +35 -1
- package/dist/history-view.d.ts +35 -1
- package/dist/history-view.js +2 -2
- package/dist/index.cjs +80 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -738
- package/dist/index.d.ts +227 -738
- package/dist/index.global.js +3 -215
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +81 -75
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -67
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +22 -22
- package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-actions-ISSKSI66.js +1 -0
- package/dist/session-actions.js +1 -0
- package/dist/session-reconnect.d.cts +180 -8
- package/dist/session-reconnect.d.ts +180 -8
- package/dist/smart-dom-reader.cjs +13 -13
- package/dist/smart-dom-reader.d.cts +180 -8
- package/dist/smart-dom-reader.d.ts +180 -8
- package/dist/smart-dom-reader.js +11 -11
- package/dist/theme-editor-preview.cjs +116 -98
- package/dist/theme-editor-preview.d.cts +182 -8
- package/dist/theme-editor-preview.d.ts +182 -8
- package/dist/theme-editor-preview.js +49 -54
- package/dist/theme-editor.cjs +2 -2
- package/dist/theme-editor.d.cts +181 -23
- package/dist/theme-editor.d.ts +181 -23
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
- package/dist/ui-extras.js +72 -0
- package/dist/voice-runtime-UOQOFAFY.js +17 -0
- package/dist/voice-runtime.cjs +17 -1
- package/dist/voice-runtime.d.cts +11650 -232
- package/dist/voice-runtime.d.ts +11650 -232
- package/dist/voice-runtime.js +17 -1
- package/dist/voice-worklet-player.cjs +19 -4
- package/dist/voice-worklet-player.d.cts +9 -10
- package/dist/voice-worklet-player.d.ts +9 -10
- package/dist/voice-worklet-player.js +19 -4
- package/dist/webmcp-polyfill.js +1 -1
- package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +1 -33
- package/dist/webmcp-runtime.d.ts +1 -33
- package/dist/webmcp-runtime.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +11 -5
- package/src/approval-paused-tool-settle.test.ts +221 -0
- package/src/client-approve.test.ts +311 -0
- package/src/client-history-eager.ts +9 -0
- package/src/client-history-loader.ts +23 -0
- package/src/client-history.ts +579 -0
- package/src/client-resume.ts +177 -0
- package/src/client-stream-eager.ts +12 -0
- package/src/client-stream-loader.ts +25 -0
- package/src/client-stream.ts +1880 -0
- package/src/client.test.ts +31 -1
- package/src/client.ts +293 -2607
- package/src/components/approval-actions.test.ts +27 -0
- package/src/components/approval-actions.ts +27 -3
- package/src/components/message-bubble.test.ts +19 -0
- package/src/components/message-bubble.ts +2 -0
- package/src/components/registry.ts +6 -6
- package/src/components/tool-bubble.test.ts +9 -0
- package/src/components/tool-bubble.ts +1 -0
- package/src/history-shell-bundle.test.ts +54 -0
- package/src/history-shell-eager.ts +12 -0
- package/src/history-shell-loader.ts +27 -0
- package/src/history-shell.ts +2362 -0
- package/src/history-view-loader.ts +2 -0
- package/src/history-view.ts +3 -0
- package/src/index-core.ts +6 -0
- package/src/index-global.ts +90 -248
- package/src/index.ts +13 -0
- package/src/install.ts +6 -4
- package/src/internal/runtype-history-provider.test.ts +1 -0
- package/src/internal/runtype-history-provider.ts +28 -13
- package/src/plugins/registry.ts +8 -8
- package/src/session-actions-chunk.test.ts +55 -0
- package/src/session-actions-eager.ts +11 -0
- package/src/session-actions-loader.ts +37 -0
- package/src/session-actions.ts +909 -0
- package/src/session-history.test.ts +3 -1
- package/src/session.ts +852 -1536
- package/src/session.voice.test.ts +1170 -5
- package/src/styles/widget.css +58 -0
- package/src/theme-editor/state.ts +91 -91
- package/src/types.ts +198 -5
- package/src/ui-ask-user-sheet.ts +382 -0
- package/src/ui-extras-eager.ts +10 -0
- package/src/ui-extras-entry.ts +9 -0
- package/src/ui-extras-loader.ts +26 -0
- package/src/ui.artifact-rail-resize.test.ts +2 -1
- package/src/ui.ask-user-chunk.test.ts +93 -0
- package/src/ui.history-chunk.test.ts +2 -1
- package/src/ui.history-config-provider.test.ts +2 -1
- package/src/ui.history-confirm-fallback.test.ts +142 -0
- package/src/ui.history-controller.test.ts +2 -1
- package/src/ui.history-lifecycle.test.ts +2 -1
- package/src/ui.history-render-hooks.test.ts +2 -1
- package/src/ui.history-shell.test.ts +2 -1
- package/src/ui.rail-overlay.test.ts +2 -1
- package/src/ui.rail-resize.test.ts +2 -1
- package/src/ui.rail-trigger-pending.test.ts +2 -1
- package/src/ui.ts +340 -2770
- package/src/utils/attachment-manager.ts +67 -67
- package/src/utils/constants.ts +3 -0
- package/src/utils/context-mention-controller.ts +323 -323
- package/src/utils/context-mention-manager.ts +40 -40
- package/src/utils/context-mention-orchestrator.ts +28 -0
- package/src/utils/event-stream-buffer.ts +50 -50
- package/src/utils/event-stream-store.ts +43 -43
- package/src/utils/throughput-tracker.ts +34 -34
- package/src/utils/virtual-scroller.ts +67 -67
- package/src/utils/visitor-store.test.ts +50 -0
- package/src/utils/visitor-store.ts +23 -5
- package/src/voice/__fixtures__/voice-wire/README.md +1 -0
- package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
- package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
- package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
- package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
- package/src/voice/audio-playback-manager.test.ts +57 -0
- package/src/voice/audio-playback-manager.ts +120 -98
- package/src/voice/browser-voice-provider.ts +37 -37
- package/src/voice/fallback-speech-engine.ts +22 -16
- package/src/voice/keyed-voice-transcript.ts +314 -0
- package/src/voice/mic-state-styles.ts +182 -0
- package/src/voice/read-aloud-controller.ts +46 -44
- package/src/voice/runtype-speech-engine.ts +35 -33
- package/src/voice/runtype-voice-provider.ts +670 -189
- package/src/voice/session-voice-wiring.ts +266 -0
- package/src/voice/voice-delegation.test.ts +71 -0
- package/src/voice/voice-delegation.ts +364 -0
- package/src/voice/voice-turn-tracker.ts +6 -6
- package/src/voice/voice.test.ts +981 -8
- package/src/voice/worklet-playback-engine.test.ts +81 -0
- package/src/voice/worklet-playback-engine.ts +21 -3
- package/src/voice-runtime-loader.ts +4 -1
- package/src/voice-runtime.ts +7 -0
- package/src/webmcp-runtime-entry.ts +41 -39
- package/dist/approval-ui-3NMYNXBD.js +0 -1
- package/dist/chunk-5EIIHQLQ.js +0 -1
- package/dist/chunk-7CO4T5KB.js +0 -1
- package/dist/chunk-EFJBUH5F.js +0 -1
- package/dist/chunk-R2TMIMDI.js +0 -3
- package/dist/chunk-UTVLOTWB.js +0 -9
- package/dist/context-mentions-45MODWOY.js +0 -161
- package/dist/event-stream-view-GL3ADRY2.js +0 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
- package/dist/voice-runtime-7DA7MJH4.js +0 -1
- package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
|
@@ -115,63 +115,63 @@ let listboxSeq = 0;
|
|
|
115
115
|
* the manager's job (reached via `onSelect`).
|
|
116
116
|
*/
|
|
117
117
|
export class ContextMentionController {
|
|
118
|
-
|
|
118
|
+
readonly #opts: ContextMentionControllerOptions;
|
|
119
119
|
/** Primary `@` channel + any extra `triggers` channels; one drives the menu. */
|
|
120
|
-
|
|
120
|
+
readonly #channels: NormalizedMentionChannel[];
|
|
121
121
|
/** The channel whose trigger is currently open. */
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
#activeChannel: NormalizedMentionChannel;
|
|
123
|
+
readonly #maxPerGroup: number;
|
|
124
|
+
readonly #debounceMs: number;
|
|
125
|
+
readonly #menu: MentionMenuParts;
|
|
126
|
+
readonly #listboxId: string;
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
#popover: PopoverHandle | null = null;
|
|
129
129
|
// Watches the composer box while the menu is open: an auto-grow line-wrap moves
|
|
130
130
|
// the `@` glyph (x changes) and shifts the composer's edges (the upward menu's
|
|
131
131
|
// top changes), so on each resize we re-measure the trigger anchor and
|
|
132
132
|
// reposition. Connected on open, disconnected on close/destroy (never leaks).
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
#resizeObserver: ResizeObserver | null = null;
|
|
134
|
+
#isOpenState = false;
|
|
135
|
+
#query = "";
|
|
136
|
+
#triggerMatch: MentionTriggerMatch | null = null;
|
|
137
|
+
#activeIndex = 0;
|
|
138
138
|
/** Identity (`sourceId, itemId`) of the highlighted row, so the highlight
|
|
139
139
|
* follows the same item when async results reorder the flat list. */
|
|
140
|
-
|
|
140
|
+
#activeKey: string | null = null;
|
|
141
141
|
// True while the menu was opened from the affordance button as a picker: no
|
|
142
142
|
// trigger char is in the textarea, so the in-menu search field owns the query
|
|
143
143
|
// and there is nothing to strip on close.
|
|
144
|
-
|
|
144
|
+
#pickerMode = false;
|
|
145
145
|
// The trigger of the channel whose button opened the current picker, so the
|
|
146
146
|
// close path can address the same button's aria-expanded even though `close()`
|
|
147
147
|
// has no channel in hand.
|
|
148
|
-
|
|
148
|
+
#pickerTrigger: string | null = null;
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
#groups: MentionMenuGroup[] = [];
|
|
151
|
+
#flat: {
|
|
152
152
|
source: AgentWidgetContextMentionSource;
|
|
153
153
|
item: AgentWidgetContextMentionItem;
|
|
154
154
|
}[] = [];
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
#searchToken = 0;
|
|
157
|
+
#searchAbort: AbortController | null = null;
|
|
158
|
+
#debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
159
|
+
readonly #knownAsync = new Set<string>();
|
|
160
160
|
// Source ids invoked in setQuery's immediate pass for the CURRENT token, so the
|
|
161
161
|
// trailing debounce doesn't re-fire a first-time async source's identical fetch.
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
readonly #invokedForToken = new Set<string>();
|
|
163
|
+
#lastAnnouncedCount = -1;
|
|
164
164
|
|
|
165
165
|
// Trigger-anchored menu positioning (inline mode). `triggerAnchorOffset` holds
|
|
166
166
|
// the trigger glyph's x/y deltas from the composer's left/top edges, measured
|
|
167
167
|
// ONCE per trigger session (see `updateTriggerAnchor`). Deltas survive scroll/pan;
|
|
168
168
|
// the popover adds them to the live anchor rect each reposition instead of
|
|
169
169
|
// re-measuring per keystroke. `x` is null in RTL (horizontal falls back).
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
#triggerAnchorOffset: { x: number | null; y: number } | null = null;
|
|
171
|
+
#measuredTriggerIndex: number | null = null;
|
|
172
172
|
|
|
173
173
|
constructor(opts: ContextMentionControllerOptions) {
|
|
174
|
-
this
|
|
174
|
+
this.#opts = opts;
|
|
175
175
|
const cfg = opts.mentionConfig;
|
|
176
176
|
// One shared normalizer (primary `@` channel + any extra `triggers`); the
|
|
177
177
|
// engine drives them all from one menu. Drop empty channels (e.g. the default
|
|
@@ -179,25 +179,25 @@ export class ContextMentionController {
|
|
|
179
179
|
// config somehow declared none (the orchestrator gates this case upstream).
|
|
180
180
|
const all = normalizeMentionChannels(cfg);
|
|
181
181
|
const nonEmpty = all.filter((c) => c.sources.length > 0);
|
|
182
|
-
this
|
|
183
|
-
this
|
|
184
|
-
this
|
|
185
|
-
this
|
|
186
|
-
this
|
|
187
|
-
|
|
188
|
-
this
|
|
189
|
-
? this
|
|
182
|
+
this.#channels = nonEmpty.length > 0 ? nonEmpty : [all[0]];
|
|
183
|
+
this.#activeChannel = this.#channels[0];
|
|
184
|
+
this.#maxPerGroup = cfg.maxItemsPerGroup ?? 6;
|
|
185
|
+
this.#debounceMs = cfg.searchDebounceMs ?? 150;
|
|
186
|
+
this.#listboxId = `persona-mention-listbox-${++listboxSeq}`;
|
|
187
|
+
|
|
188
|
+
this.#menu = opts.mentionConfig.renderMentionMenu
|
|
189
|
+
? this.#createHostMenu()
|
|
190
190
|
: createMentionMenu({
|
|
191
191
|
config: opts.mentionConfig,
|
|
192
|
-
listboxId: this
|
|
193
|
-
onSelectIndex: (i) => this
|
|
192
|
+
listboxId: this.#listboxId,
|
|
193
|
+
onSelectIndex: (i) => this.#selectIndex(i),
|
|
194
194
|
// Hover highlights without scrolling (keyboard scrolls; a moving cursor
|
|
195
195
|
// shouldn't yank the list).
|
|
196
|
-
onHoverIndex: (i) => this
|
|
197
|
-
onRetry: (sourceId) => this
|
|
196
|
+
onHoverIndex: (i) => this.#setActiveIndex(i, false),
|
|
197
|
+
onRetry: (sourceId) => this.#retrySource(sourceId),
|
|
198
198
|
// Picker mode: the in-menu search field is the query source and drives
|
|
199
199
|
// keyboard nav, since focus lives in it (not the textarea).
|
|
200
|
-
onSearchInput: (value) => this
|
|
200
|
+
onSearchInput: (value) => this.#setQuery(value),
|
|
201
201
|
onSearchKeydown: (event) => {
|
|
202
202
|
this.handleKeydown(event);
|
|
203
203
|
},
|
|
@@ -205,7 +205,7 @@ export class ContextMentionController {
|
|
|
205
205
|
|
|
206
206
|
const el = opts.composerInput.element;
|
|
207
207
|
el.setAttribute("aria-haspopup", "listbox");
|
|
208
|
-
el.setAttribute("aria-controls", this
|
|
208
|
+
el.setAttribute("aria-controls", this.#listboxId);
|
|
209
209
|
// NB: no aria-expanded here. The editable surface keeps role="textbox" +
|
|
210
210
|
// aria-multiline (so it retains multiline editing semantics), and
|
|
211
211
|
// aria-expanded is not a supported state on role="textbox" — promoting it to
|
|
@@ -216,26 +216,26 @@ export class ContextMentionController {
|
|
|
216
216
|
|
|
217
217
|
/** The composer input surface — all text/selection ops route through this. */
|
|
218
218
|
private get input(): ComposerInputCapability {
|
|
219
|
-
return this
|
|
219
|
+
return this.#opts.composerInput;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// The host-render path wraps `renderMentionMenu` output in a positioned shell;
|
|
223
223
|
// positioning, trigger detection, search, and keyboard stay ours. The host
|
|
224
224
|
// owns its own highlight, so `setActiveIndex` just re-renders.
|
|
225
|
-
|
|
225
|
+
#createHostMenu(): MentionMenuParts {
|
|
226
226
|
const el = document.createElement("div");
|
|
227
227
|
el.setAttribute("data-persona-mention-menu", "");
|
|
228
228
|
el.setAttribute("role", "listbox");
|
|
229
|
-
el.id = this
|
|
229
|
+
el.id = this.#listboxId;
|
|
230
230
|
const paint = () => {
|
|
231
|
-
const custom = this
|
|
232
|
-
query: this
|
|
233
|
-
groups: this
|
|
234
|
-
status: Object.fromEntries(this
|
|
235
|
-
activeIndex: this
|
|
231
|
+
const custom = this.#opts.mentionConfig.renderMentionMenu!({
|
|
232
|
+
query: this.#query,
|
|
233
|
+
groups: this.#groups.map((g) => ({ source: g.source, items: g.items })),
|
|
234
|
+
status: Object.fromEntries(this.#groups.map((g) => [g.source.id, g.status])),
|
|
235
|
+
activeIndex: this.#activeIndex,
|
|
236
236
|
select: (item) => {
|
|
237
|
-
const group = this
|
|
238
|
-
if (group) this
|
|
237
|
+
const group = this.#groups.find((g) => g.items.includes(item));
|
|
238
|
+
if (group) this.#commit(group.source, item);
|
|
239
239
|
},
|
|
240
240
|
close: () => this.close(),
|
|
241
241
|
});
|
|
@@ -250,7 +250,7 @@ export class ContextMentionController {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
isOpen(): boolean {
|
|
253
|
-
return this
|
|
253
|
+
return this.#isOpenState;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
@@ -266,8 +266,8 @@ export class ContextMentionController {
|
|
|
266
266
|
*/
|
|
267
267
|
openFromButton(trigger?: string): void {
|
|
268
268
|
const channel =
|
|
269
|
-
(trigger && this
|
|
270
|
-
this
|
|
269
|
+
(trigger && this.#channels.find((c) => c.trigger === trigger)) ||
|
|
270
|
+
this.#channels[0];
|
|
271
271
|
|
|
272
272
|
// If a live trigger token for THIS channel already sits at the caret (the
|
|
273
273
|
// user typed `@que` then clicked the button), adopt it as a normal typed
|
|
@@ -276,13 +276,13 @@ export class ContextMentionController {
|
|
|
276
276
|
const caret = this.input.getSelection().start;
|
|
277
277
|
const typed = parseAnyTrigger(this.input.getLogicalText(), caret, [channel]);
|
|
278
278
|
if (typed) {
|
|
279
|
-
this
|
|
280
|
-
this
|
|
281
|
-
if (!this
|
|
282
|
-
this
|
|
279
|
+
this.#pickerMode = false;
|
|
280
|
+
this.#triggerMatch = typed.match;
|
|
281
|
+
if (!this.#isOpenState) {
|
|
282
|
+
this.#open(typed.match.query, channel);
|
|
283
283
|
} else {
|
|
284
|
-
this
|
|
285
|
-
this
|
|
284
|
+
this.#switchChannel(channel);
|
|
285
|
+
this.#setQuery(typed.match.query);
|
|
286
286
|
}
|
|
287
287
|
this.input.focus();
|
|
288
288
|
return;
|
|
@@ -291,90 +291,90 @@ export class ContextMentionController {
|
|
|
291
291
|
// Switching to a different channel's picker while one is already open must
|
|
292
292
|
// close the previous button's state first; close() later emits false for the
|
|
293
293
|
// NEW trigger only, so the old button's aria-expanded would stick true.
|
|
294
|
-
const prevPickerTrigger = this
|
|
295
|
-
this
|
|
296
|
-
this
|
|
297
|
-
this
|
|
298
|
-
if (!this
|
|
299
|
-
this
|
|
294
|
+
const prevPickerTrigger = this.#pickerTrigger;
|
|
295
|
+
this.#pickerMode = true;
|
|
296
|
+
this.#pickerTrigger = channel.trigger;
|
|
297
|
+
this.#triggerMatch = null;
|
|
298
|
+
if (!this.#isOpenState) {
|
|
299
|
+
this.#open("", channel);
|
|
300
300
|
} else {
|
|
301
301
|
if (prevPickerTrigger && prevPickerTrigger !== channel.trigger) {
|
|
302
|
-
this
|
|
302
|
+
this.#opts.onPickerOpenChange?.(false, prevPickerTrigger, this.#listboxId);
|
|
303
303
|
}
|
|
304
|
-
this
|
|
305
|
-
this
|
|
304
|
+
this.#switchChannel(channel);
|
|
305
|
+
this.#setQuery("");
|
|
306
306
|
}
|
|
307
|
-
if (this
|
|
307
|
+
if (this.#menu.showSearch) this.#menu.showSearch("", channel.searchPlaceholder);
|
|
308
308
|
else this.input.focus();
|
|
309
|
-
this
|
|
309
|
+
this.#opts.onPickerOpenChange?.(true, channel.trigger, this.#listboxId);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/** Re-parse the composer on every input and open/update/close the menu. */
|
|
313
313
|
onInput(): void {
|
|
314
314
|
const caret = this.input.getSelection().start;
|
|
315
|
-
const hit = parseAnyTrigger(this.input.getLogicalText(), caret, this
|
|
315
|
+
const hit = parseAnyTrigger(this.input.getLogicalText(), caret, this.#channels);
|
|
316
316
|
if (!hit) {
|
|
317
|
-
if (this
|
|
317
|
+
if (this.#isOpenState) this.close();
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
|
-
if (this
|
|
320
|
+
if (this.#isInlineArgTail(hit.channel, hit.match.query)) {
|
|
321
321
|
// The command name is complete and the caret is into the argument — the
|
|
322
322
|
// menu has nothing left to offer, so keep it closed (Slack-style). The
|
|
323
323
|
// command runs at submit via `dispatchInlineCommand`.
|
|
324
|
-
this
|
|
325
|
-
if (this
|
|
324
|
+
this.#triggerMatch = null;
|
|
325
|
+
if (this.#isOpenState) this.close(false);
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
|
-
this
|
|
329
|
-
if (!this
|
|
330
|
-
this
|
|
328
|
+
this.#triggerMatch = hit.match;
|
|
329
|
+
if (!this.#isOpenState) {
|
|
330
|
+
this.#open(hit.match.query, hit.channel);
|
|
331
331
|
} else {
|
|
332
|
-
this
|
|
332
|
+
this.#switchChannel(hit.channel);
|
|
333
333
|
// Re-anchor only when the trigger MOVED (new session / edit before the `@`).
|
|
334
334
|
// Plain query typing keeps the same `triggerIndex`, so the menu stays put —
|
|
335
335
|
// no per-keystroke layout work (Slack behavior).
|
|
336
|
-
if (this
|
|
337
|
-
this
|
|
336
|
+
if (this.#measuredTriggerIndex !== hit.match.triggerIndex) {
|
|
337
|
+
this.#updateTriggerAnchor();
|
|
338
338
|
}
|
|
339
|
-
this
|
|
339
|
+
this.#setQuery(hit.match.query);
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/** Switch the active channel while open, resetting stale groups from the old one. */
|
|
344
|
-
|
|
345
|
-
if (channel === this
|
|
346
|
-
this
|
|
347
|
-
this
|
|
348
|
-
this
|
|
344
|
+
#switchChannel(channel: NormalizedMentionChannel): void {
|
|
345
|
+
if (channel === this.#activeChannel) return;
|
|
346
|
+
this.#activeChannel = channel;
|
|
347
|
+
this.#groups = [];
|
|
348
|
+
this.#activeIndex = 0;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
this
|
|
353
|
-
this
|
|
354
|
-
this
|
|
355
|
-
this
|
|
356
|
-
this
|
|
357
|
-
if (!this
|
|
351
|
+
#open(query: string, channel: NormalizedMentionChannel): void {
|
|
352
|
+
this.#isOpenState = true;
|
|
353
|
+
this.#activeChannel = channel;
|
|
354
|
+
this.#groups = [];
|
|
355
|
+
this.#activeIndex = 0;
|
|
356
|
+
this.#lastAnnouncedCount = -1;
|
|
357
|
+
if (!this.#popover) {
|
|
358
358
|
// Inline mode with a measurable composer trigger-anchors the menu to the `@`
|
|
359
359
|
// glyph (Slack-style): the menu is content-sized and shifts horizontally.
|
|
360
360
|
// Every other mode keeps the composer-anchored, full-width menu unchanged.
|
|
361
|
-
const anchored = this
|
|
362
|
-
this
|
|
363
|
-
anchor: this
|
|
364
|
-
content: this
|
|
361
|
+
const anchored = this.#canAnchorMenu();
|
|
362
|
+
this.#popover = createPopover({
|
|
363
|
+
anchor: this.#opts.anchor,
|
|
364
|
+
content: this.#menu.el,
|
|
365
365
|
placement: "top-start",
|
|
366
366
|
// Full-width menu only in composer-anchored mode; the trigger-anchored
|
|
367
367
|
// menu must be content-sized so it has room to shift within the composer.
|
|
368
368
|
matchAnchorWidth: !anchored,
|
|
369
369
|
offset: 6,
|
|
370
|
-
container: this
|
|
370
|
+
container: this.#opts.popoverContainer,
|
|
371
371
|
// Trigger-anchored offsets (px from the composer's left/top edges), or
|
|
372
372
|
// null → composer-anchored fallback on that axis. Omitted when not anchored.
|
|
373
373
|
horizontalOffset: anchored
|
|
374
|
-
? () => this
|
|
374
|
+
? () => this.#triggerAnchorOffset?.x ?? null
|
|
375
375
|
: undefined,
|
|
376
376
|
verticalOffset: anchored
|
|
377
|
-
? () => this
|
|
377
|
+
? () => this.#triggerAnchorOffset?.y ?? null
|
|
378
378
|
: undefined,
|
|
379
379
|
// Outside-click / anchor-removed: run the SAME teardown as an explicit
|
|
380
380
|
// close so the debounce timer, in-flight search, and search token are
|
|
@@ -389,23 +389,23 @@ export class ContextMentionController {
|
|
|
389
389
|
// resolve against the viewport (the popover is fixed-positioned) and, as
|
|
390
390
|
// min-width beats max-width, overflow composers narrower than 220px.
|
|
391
391
|
if (anchored) {
|
|
392
|
-
const anchorWidth = this
|
|
393
|
-
this
|
|
392
|
+
const anchorWidth = this.#opts.anchor.getBoundingClientRect().width;
|
|
393
|
+
this.#menu.el.style.minWidth = `${Math.min(220, anchorWidth)}px`;
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
396
|
// Measure the trigger glyph ONCE for this session (before the first reposition
|
|
397
397
|
// in setQuery). Re-measures only on a trigger-index change (new session); the
|
|
398
398
|
// popover's own scroll/resize reposition reuses the cached anchor-relative
|
|
399
399
|
// delta, so panning/scrolling stays aligned without re-measuring.
|
|
400
|
-
this
|
|
401
|
-
this
|
|
400
|
+
this.#updateTriggerAnchor();
|
|
401
|
+
this.#popover.open();
|
|
402
402
|
// Inject the menu CSS into whatever root the menu now lives in (document
|
|
403
403
|
// head by default; the shadow root under `useShadowDom`). Idempotent per
|
|
404
404
|
// root — this chunk carries the menu styles instead of the eager widget.css.
|
|
405
|
-
injectStyles(this
|
|
406
|
-
this
|
|
407
|
-
this
|
|
408
|
-
this
|
|
405
|
+
injectStyles(this.#menu.el, "persona-mention-menu", MENTION_MENU_CSS);
|
|
406
|
+
this.#observeComposerResize();
|
|
407
|
+
this.#opts.emit?.("opened", { trigger: channel.trigger });
|
|
408
|
+
this.#setQuery(query);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
/**
|
|
@@ -418,19 +418,19 @@ export class ContextMentionController {
|
|
|
418
418
|
* there is no observer feedback loop. Degrades silently where ResizeObserver is
|
|
419
419
|
* unavailable (e.g. jsdom), so behavior falls back to scroll/window-resize only.
|
|
420
420
|
*/
|
|
421
|
-
|
|
422
|
-
if (typeof ResizeObserver === "undefined" || this
|
|
423
|
-
this
|
|
424
|
-
if (!this
|
|
425
|
-
this
|
|
426
|
-
this
|
|
421
|
+
#observeComposerResize(): void {
|
|
422
|
+
if (typeof ResizeObserver === "undefined" || this.#resizeObserver) return;
|
|
423
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
424
|
+
if (!this.#isOpenState) return;
|
|
425
|
+
this.#updateTriggerAnchor();
|
|
426
|
+
this.#popover?.reposition();
|
|
427
427
|
});
|
|
428
|
-
this
|
|
428
|
+
this.#resizeObserver.observe(this.#opts.anchor);
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
|
|
432
|
-
this
|
|
433
|
-
this
|
|
431
|
+
#disconnectComposerResize(): void {
|
|
432
|
+
this.#resizeObserver?.disconnect();
|
|
433
|
+
this.#resizeObserver = null;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
/**
|
|
@@ -438,8 +438,8 @@ export class ContextMentionController {
|
|
|
438
438
|
* composer that can measure a logical range rect. Chip/textarea mode (no
|
|
439
439
|
* `getLogicalRangeRect`) keeps the composer-anchored menu — graceful degradation.
|
|
440
440
|
*/
|
|
441
|
-
|
|
442
|
-
return this
|
|
441
|
+
#canAnchorMenu(): boolean {
|
|
442
|
+
return this.#isInlineDisplay() && !!this.input.getLogicalRangeRect;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
/**
|
|
@@ -454,136 +454,136 @@ export class ContextMentionController {
|
|
|
454
454
|
* `triggerIndex`; a non-collapsed range around it measures reliably (a collapsed
|
|
455
455
|
* boundary range measures empty).
|
|
456
456
|
*/
|
|
457
|
-
|
|
458
|
-
const match = this
|
|
457
|
+
#updateTriggerAnchor(): void {
|
|
458
|
+
const match = this.#triggerMatch;
|
|
459
459
|
const measure = this.input.getLogicalRangeRect;
|
|
460
|
-
this
|
|
461
|
-
this
|
|
460
|
+
this.#measuredTriggerIndex = match?.triggerIndex ?? null;
|
|
461
|
+
this.#triggerAnchorOffset = null;
|
|
462
462
|
if (!match || !measure) return;
|
|
463
463
|
const rect = measure(match.triggerIndex, match.triggerIndex + 1);
|
|
464
464
|
if (!rect) return;
|
|
465
|
-
const anchorRect = this
|
|
465
|
+
const anchorRect = this.#opts.anchor.getBoundingClientRect();
|
|
466
466
|
const y = rect.top - anchorRect.top;
|
|
467
467
|
const el = this.input.element;
|
|
468
468
|
const rtl =
|
|
469
469
|
typeof getComputedStyle === "function" &&
|
|
470
470
|
getComputedStyle(el).direction === "rtl";
|
|
471
471
|
// RTL: horizontal trigger anchoring is left-to-right math; fall back on x only.
|
|
472
|
-
this
|
|
472
|
+
this.#triggerAnchorOffset = {
|
|
473
473
|
x: rtl ? null : rect.left - anchorRect.left,
|
|
474
474
|
y,
|
|
475
475
|
};
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
close(refocus = true): void {
|
|
479
|
-
if (!this
|
|
480
|
-
this
|
|
481
|
-
this
|
|
482
|
-
if (this
|
|
483
|
-
this
|
|
479
|
+
if (!this.#isOpenState) return;
|
|
480
|
+
this.#isOpenState = false;
|
|
481
|
+
this.#disconnectComposerResize();
|
|
482
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
483
|
+
this.#searchAbort?.abort();
|
|
484
484
|
// Invalidate any in-flight async search so late results can't render,
|
|
485
485
|
// announce, or emit into a closed menu.
|
|
486
|
-
this
|
|
487
|
-
this
|
|
486
|
+
this.#searchToken++;
|
|
487
|
+
this.#popover?.close();
|
|
488
488
|
this.input.element.removeAttribute("aria-activedescendant");
|
|
489
|
-
if (this
|
|
489
|
+
if (this.#pickerMode) {
|
|
490
490
|
// Picker teardown: hide the search field and (unless dismissed by an
|
|
491
491
|
// outside click) hand focus back to the composer so the user keeps typing.
|
|
492
|
-
this
|
|
493
|
-
this
|
|
494
|
-
this
|
|
492
|
+
this.#pickerMode = false;
|
|
493
|
+
this.#menu.hideSearch?.();
|
|
494
|
+
this.#opts.onPickerOpenChange?.(
|
|
495
495
|
false,
|
|
496
|
-
this
|
|
497
|
-
this
|
|
496
|
+
this.#pickerTrigger ?? this.#activeChannel.trigger,
|
|
497
|
+
this.#listboxId
|
|
498
498
|
);
|
|
499
|
-
this
|
|
499
|
+
this.#pickerTrigger = null;
|
|
500
500
|
if (refocus) this.input.focus();
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
this
|
|
504
|
+
#setQuery(query: string): void {
|
|
505
|
+
this.#query = query;
|
|
506
506
|
// A new query resets the highlight to the first result (standard
|
|
507
507
|
// autocomplete). Async results for the SAME query keep it via `activeKey`.
|
|
508
|
-
this
|
|
509
|
-
this
|
|
510
|
-
const token = ++this
|
|
511
|
-
this
|
|
512
|
-
this
|
|
513
|
-
this
|
|
508
|
+
this.#activeIndex = 0;
|
|
509
|
+
this.#activeKey = null;
|
|
510
|
+
const token = ++this.#searchToken;
|
|
511
|
+
this.#invokedForToken.clear();
|
|
512
|
+
this.#searchAbort?.abort();
|
|
513
|
+
this.#searchAbort = new AbortController();
|
|
514
514
|
|
|
515
515
|
// Immediate pass: sync sources (and first-time sources) render with zero
|
|
516
516
|
// debounce; sources already known to be async show a loading shimmer.
|
|
517
|
-
for (const source of this
|
|
518
|
-
if (this
|
|
519
|
-
this
|
|
517
|
+
for (const source of this.#activeChannel.sources) {
|
|
518
|
+
if (this.#knownAsync.has(source.id)) {
|
|
519
|
+
this.#setGroupStatus(source.id, "loading");
|
|
520
520
|
} else {
|
|
521
|
-
this
|
|
522
|
-
this
|
|
521
|
+
this.#invokedForToken.add(source.id);
|
|
522
|
+
this.#invokeSource(source, token);
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
|
-
this
|
|
525
|
+
this.#render();
|
|
526
526
|
|
|
527
527
|
// Debounced pass: re-invoke the known-async (network) sources, skipping any a
|
|
528
528
|
// first-time invoke already fired for this token (invokeSource marks a source
|
|
529
529
|
// async on its first promise — re-invoking here would double-fetch it).
|
|
530
|
-
if (this
|
|
531
|
-
this
|
|
532
|
-
if (token !== this
|
|
533
|
-
for (const source of this
|
|
534
|
-
if (this
|
|
535
|
-
this
|
|
530
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
531
|
+
this.#debounceTimer = setTimeout(() => {
|
|
532
|
+
if (token !== this.#searchToken) return;
|
|
533
|
+
for (const source of this.#activeChannel.sources) {
|
|
534
|
+
if (this.#knownAsync.has(source.id) && !this.#invokedForToken.has(source.id)) {
|
|
535
|
+
this.#invokeSource(source, token);
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
|
-
}, this
|
|
538
|
+
}, this.#debounceMs);
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
|
|
541
|
+
#invokeSource(
|
|
542
542
|
source: AgentWidgetContextMentionSource,
|
|
543
543
|
token: number
|
|
544
544
|
): void {
|
|
545
545
|
const ctx = {
|
|
546
|
-
messages: this
|
|
547
|
-
config: this
|
|
548
|
-
signal: this
|
|
546
|
+
messages: this.#opts.getMessages(),
|
|
547
|
+
config: this.#opts.getConfig(),
|
|
548
|
+
signal: this.#searchAbort!.signal,
|
|
549
549
|
};
|
|
550
550
|
let result:
|
|
551
551
|
| AgentWidgetContextMentionItem[]
|
|
552
552
|
| Promise<AgentWidgetContextMentionItem[]>;
|
|
553
553
|
try {
|
|
554
|
-
result = source.search(this
|
|
554
|
+
result = source.search(this.#query, ctx);
|
|
555
555
|
} catch {
|
|
556
|
-
this
|
|
557
|
-
this
|
|
556
|
+
this.#setGroupStatus(source.id, "error");
|
|
557
|
+
this.#render();
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
if (isThenable(result)) {
|
|
561
|
-
this
|
|
562
|
-
this
|
|
561
|
+
this.#knownAsync.add(source.id);
|
|
562
|
+
this.#setGroupStatus(source.id, "loading");
|
|
563
563
|
result
|
|
564
564
|
.then((items) => {
|
|
565
|
-
if (token !== this
|
|
566
|
-
this
|
|
567
|
-
this
|
|
565
|
+
if (token !== this.#searchToken) return;
|
|
566
|
+
this.#setGroupItems(source.id, items as AgentWidgetContextMentionItem[]);
|
|
567
|
+
this.#render();
|
|
568
568
|
})
|
|
569
569
|
.catch(() => {
|
|
570
|
-
if (token !== this
|
|
571
|
-
this
|
|
572
|
-
this
|
|
570
|
+
if (token !== this.#searchToken) return;
|
|
571
|
+
this.#setGroupStatus(source.id, "error");
|
|
572
|
+
this.#render();
|
|
573
573
|
});
|
|
574
574
|
} else {
|
|
575
|
-
this
|
|
575
|
+
this.#setGroupItems(source.id, result);
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
-
|
|
580
|
-
let group = this
|
|
579
|
+
#getOrCreateGroup(source: AgentWidgetContextMentionSource): MentionMenuGroup {
|
|
580
|
+
let group = this.#groups.find((g) => g.source.id === source.id);
|
|
581
581
|
if (!group) {
|
|
582
582
|
group = { source, items: [], status: "loading", truncated: false };
|
|
583
583
|
// Preserve source declaration order within the active channel.
|
|
584
|
-
const order = this
|
|
585
|
-
this
|
|
586
|
-
this
|
|
584
|
+
const order = this.#activeChannel.sources;
|
|
585
|
+
this.#groups.push(group);
|
|
586
|
+
this.#groups.sort(
|
|
587
587
|
(a, b) =>
|
|
588
588
|
order.findIndex((s) => s.id === a.source.id) -
|
|
589
589
|
order.findIndex((s) => s.id === b.source.id)
|
|
@@ -592,85 +592,85 @@ export class ContextMentionController {
|
|
|
592
592
|
return group;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
-
|
|
596
|
-
const source = this
|
|
595
|
+
#setGroupStatus(sourceId: string, status: MentionMenuGroup["status"]): void {
|
|
596
|
+
const source = this.#activeChannel.sources.find((s) => s.id === sourceId);
|
|
597
597
|
if (!source) return;
|
|
598
|
-
this
|
|
598
|
+
this.#getOrCreateGroup(source).status = status;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
#setGroupItems(
|
|
602
602
|
sourceId: string,
|
|
603
603
|
items: AgentWidgetContextMentionItem[]
|
|
604
604
|
): void {
|
|
605
|
-
const source = this
|
|
605
|
+
const source = this.#activeChannel.sources.find((s) => s.id === sourceId);
|
|
606
606
|
if (!source) return;
|
|
607
|
-
const group = this
|
|
608
|
-
group.truncated = items.length > this
|
|
609
|
-
group.items = items.slice(0, this
|
|
607
|
+
const group = this.#getOrCreateGroup(source);
|
|
608
|
+
group.truncated = items.length > this.#maxPerGroup;
|
|
609
|
+
group.items = items.slice(0, this.#maxPerGroup);
|
|
610
610
|
group.status = group.items.length === 0 ? "empty" : "ready";
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
/** Stable identity for a flat entry (for tracking the highlight across reorders). */
|
|
614
|
-
|
|
614
|
+
#keyOf(entry: {
|
|
615
615
|
source: AgentWidgetContextMentionSource;
|
|
616
616
|
item: AgentWidgetContextMentionItem;
|
|
617
617
|
}): string {
|
|
618
618
|
return JSON.stringify([entry.source.id, entry.item.id]);
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
-
|
|
622
|
-
this
|
|
623
|
-
for (const group of this
|
|
621
|
+
#rebuildFlat(): void {
|
|
622
|
+
this.#flat = [];
|
|
623
|
+
for (const group of this.#groups) {
|
|
624
624
|
if (group.status === "ready") {
|
|
625
|
-
for (const item of group.items) this
|
|
625
|
+
for (const item of group.items) this.#flat.push({ source: group.source, item });
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
// Keep the highlight on the same item when async results reorder the list;
|
|
629
629
|
// fall back to the clamped index (or 0) when that item is no longer present.
|
|
630
|
-
if (this
|
|
631
|
-
const idx = this
|
|
632
|
-
this
|
|
633
|
-
idx >= 0 ? idx : Math.min(this
|
|
634
|
-
} else if (this
|
|
635
|
-
this
|
|
630
|
+
if (this.#activeKey) {
|
|
631
|
+
const idx = this.#flat.findIndex((e) => this.#keyOf(e) === this.#activeKey);
|
|
632
|
+
this.#activeIndex =
|
|
633
|
+
idx >= 0 ? idx : Math.min(this.#activeIndex, Math.max(0, this.#flat.length - 1));
|
|
634
|
+
} else if (this.#activeIndex >= this.#flat.length) {
|
|
635
|
+
this.#activeIndex = Math.max(0, this.#flat.length - 1);
|
|
636
636
|
}
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
|
|
639
|
+
#viewModel() {
|
|
640
640
|
return {
|
|
641
|
-
query: this
|
|
642
|
-
groups: this
|
|
643
|
-
activeIndex: this
|
|
641
|
+
query: this.#query,
|
|
642
|
+
groups: this.#groups,
|
|
643
|
+
activeIndex: this.#activeIndex,
|
|
644
644
|
};
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
|
|
648
|
-
this
|
|
649
|
-
this
|
|
650
|
-
this
|
|
651
|
-
this
|
|
647
|
+
#render(): void {
|
|
648
|
+
this.#rebuildFlat();
|
|
649
|
+
this.#menu.render(this.#viewModel());
|
|
650
|
+
this.#syncActiveDescendant();
|
|
651
|
+
this.#popover?.reposition();
|
|
652
652
|
// Don't announce "No matches" while results are still loading in.
|
|
653
|
-
const anyLoading = this
|
|
654
|
-
if (this
|
|
655
|
-
if (this
|
|
656
|
-
this
|
|
657
|
-
this
|
|
658
|
-
this
|
|
653
|
+
const anyLoading = this.#groups.some((g) => g.status === "loading");
|
|
654
|
+
if (this.#flat.length === 0 && anyLoading) return;
|
|
655
|
+
if (this.#flat.length !== this.#lastAnnouncedCount) {
|
|
656
|
+
this.#lastAnnouncedCount = this.#flat.length;
|
|
657
|
+
this.#opts.announce(
|
|
658
|
+
this.#flat.length === 0
|
|
659
659
|
? "No matches"
|
|
660
|
-
: this
|
|
660
|
+
: this.#flat.length === 1
|
|
661
661
|
? "1 result"
|
|
662
|
-
: `${this
|
|
662
|
+
: `${this.#flat.length} results`
|
|
663
663
|
);
|
|
664
|
-
this
|
|
664
|
+
this.#opts.emit?.("searched", { query: this.#query, results: this.#flat.length });
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
|
|
669
|
-
if (index < 0 || index >= this
|
|
670
|
-
this
|
|
671
|
-
this
|
|
672
|
-
this
|
|
673
|
-
this
|
|
668
|
+
#setActiveIndex(index: number, scroll = true): void {
|
|
669
|
+
if (index < 0 || index >= this.#flat.length) return;
|
|
670
|
+
this.#activeIndex = index;
|
|
671
|
+
this.#activeKey = this.#keyOf(this.#flat[index]);
|
|
672
|
+
this.#menu.setActiveIndex(index, scroll);
|
|
673
|
+
this.#syncActiveDescendant();
|
|
674
674
|
}
|
|
675
675
|
|
|
676
676
|
/**
|
|
@@ -679,16 +679,16 @@ export class ContextMentionController {
|
|
|
679
679
|
* there). In picker mode the search field owns focus + `aria-activedescendant`,
|
|
680
680
|
* so the textarea's is cleared.
|
|
681
681
|
*/
|
|
682
|
-
|
|
682
|
+
#syncActiveDescendant(): void {
|
|
683
683
|
const live =
|
|
684
|
-
this
|
|
685
|
-
!this
|
|
686
|
-
this
|
|
687
|
-
this
|
|
684
|
+
this.#isOpenState &&
|
|
685
|
+
!this.#pickerMode &&
|
|
686
|
+
this.#activeIndex >= 0 &&
|
|
687
|
+
this.#activeIndex < this.#flat.length;
|
|
688
688
|
if (live) {
|
|
689
689
|
this.input.element.setAttribute(
|
|
690
690
|
"aria-activedescendant",
|
|
691
|
-
`${this
|
|
691
|
+
`${this.#listboxId}-opt-${this.#activeIndex}`
|
|
692
692
|
);
|
|
693
693
|
} else {
|
|
694
694
|
this.input.element.removeAttribute("aria-activedescendant");
|
|
@@ -696,56 +696,56 @@ export class ContextMentionController {
|
|
|
696
696
|
}
|
|
697
697
|
|
|
698
698
|
/** Re-run a failed source's search for the current query (menu Retry button). */
|
|
699
|
-
|
|
700
|
-
const source = this
|
|
701
|
-
if (!source || !this
|
|
702
|
-
this
|
|
703
|
-
this
|
|
704
|
-
this
|
|
699
|
+
#retrySource(sourceId: string): void {
|
|
700
|
+
const source = this.#activeChannel.sources.find((s) => s.id === sourceId);
|
|
701
|
+
if (!source || !this.#searchAbort) return;
|
|
702
|
+
this.#setGroupStatus(sourceId, "loading");
|
|
703
|
+
this.#render();
|
|
704
|
+
this.#invokeSource(source, this.#searchToken);
|
|
705
705
|
// Reflect a synchronous result immediately (async results render on arrival).
|
|
706
|
-
this
|
|
706
|
+
this.#render();
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
/** Returns true when the key was consumed (caller must not also handle it). */
|
|
710
710
|
handleKeydown(event: KeyboardEvent): boolean {
|
|
711
|
-
if (!this
|
|
711
|
+
if (!this.#isOpenState) return false;
|
|
712
712
|
|
|
713
713
|
switch (event.key) {
|
|
714
714
|
case "ArrowDown": {
|
|
715
|
-
if (this
|
|
715
|
+
if (this.#flat.length === 0) return true;
|
|
716
716
|
event.preventDefault();
|
|
717
|
-
this
|
|
717
|
+
this.#setActiveIndex((this.#activeIndex + 1) % this.#flat.length);
|
|
718
718
|
return true;
|
|
719
719
|
}
|
|
720
720
|
case "ArrowUp": {
|
|
721
|
-
if (this
|
|
721
|
+
if (this.#flat.length === 0) return true;
|
|
722
722
|
event.preventDefault();
|
|
723
|
-
this
|
|
724
|
-
(this
|
|
723
|
+
this.#setActiveIndex(
|
|
724
|
+
(this.#activeIndex - 1 + this.#flat.length) % this.#flat.length
|
|
725
725
|
);
|
|
726
726
|
return true;
|
|
727
727
|
}
|
|
728
728
|
case "Home": {
|
|
729
|
-
if (this
|
|
729
|
+
if (this.#flat.length === 0) return true;
|
|
730
730
|
event.preventDefault();
|
|
731
|
-
this
|
|
731
|
+
this.#setActiveIndex(0);
|
|
732
732
|
return true;
|
|
733
733
|
}
|
|
734
734
|
case "End": {
|
|
735
|
-
if (this
|
|
735
|
+
if (this.#flat.length === 0) return true;
|
|
736
736
|
event.preventDefault();
|
|
737
|
-
this
|
|
737
|
+
this.#setActiveIndex(this.#flat.length - 1);
|
|
738
738
|
return true;
|
|
739
739
|
}
|
|
740
740
|
case "Enter":
|
|
741
741
|
case "Tab": {
|
|
742
|
-
if (this
|
|
742
|
+
if (this.#flat.length === 0) {
|
|
743
743
|
// Nothing to pick — close and let Enter submit / Tab move on.
|
|
744
744
|
this.close();
|
|
745
745
|
return false;
|
|
746
746
|
}
|
|
747
747
|
event.preventDefault();
|
|
748
|
-
this
|
|
748
|
+
this.#selectIndex(this.#activeIndex);
|
|
749
749
|
return true;
|
|
750
750
|
}
|
|
751
751
|
case "Escape": {
|
|
@@ -757,7 +757,7 @@ export class ContextMentionController {
|
|
|
757
757
|
case "Backspace": {
|
|
758
758
|
// Deleting back through the trigger closes the menu; let the keystroke
|
|
759
759
|
// proceed so the trigger char itself is removed.
|
|
760
|
-
if (this
|
|
760
|
+
if (this.#query.length === 0) this.close();
|
|
761
761
|
return false;
|
|
762
762
|
}
|
|
763
763
|
default:
|
|
@@ -765,10 +765,10 @@ export class ContextMentionController {
|
|
|
765
765
|
}
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
-
|
|
769
|
-
const entry = this
|
|
768
|
+
#selectIndex(index: number): void {
|
|
769
|
+
const entry = this.#flat[index];
|
|
770
770
|
if (!entry) return;
|
|
771
|
-
this
|
|
771
|
+
this.#commit(entry.source, entry.item);
|
|
772
772
|
}
|
|
773
773
|
|
|
774
774
|
/**
|
|
@@ -777,7 +777,7 @@ export class ContextMentionController {
|
|
|
777
777
|
* token (via the same {@link splitCommandQuery}), so the item stays selectable
|
|
778
778
|
* while the user types args.
|
|
779
779
|
*/
|
|
780
|
-
|
|
780
|
+
#deriveArgs(query: string): string {
|
|
781
781
|
return splitCommandQuery(query).args;
|
|
782
782
|
}
|
|
783
783
|
|
|
@@ -787,11 +787,11 @@ export class ContextMentionController {
|
|
|
787
787
|
* arg placeholder. Selecting these fills `/name ` into the composer for inline
|
|
788
788
|
* arg entry instead of dispatching now; execution happens at submit.
|
|
789
789
|
*/
|
|
790
|
-
|
|
790
|
+
#isInlineCommand(item: AgentWidgetContextMentionItem): boolean {
|
|
791
791
|
return item.command === "server" || item.commandArgsPlaceholder != null;
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
|
|
794
|
+
#commit(
|
|
795
795
|
source: AgentWidgetContextMentionSource,
|
|
796
796
|
item: AgentWidgetContextMentionItem
|
|
797
797
|
): void {
|
|
@@ -800,18 +800,18 @@ export class ContextMentionController {
|
|
|
800
800
|
// Inline completion (Slack-style): fill `/name ` and let the user type the
|
|
801
801
|
// argument. No chip, no dispatch now — `dispatchInlineCommand` runs it at
|
|
802
802
|
// submit with the typed args.
|
|
803
|
-
if (this
|
|
804
|
-
this
|
|
803
|
+
if (this.#isInlineCommand(item)) {
|
|
804
|
+
this.#completeCommandInline(source, item);
|
|
805
805
|
return;
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
// (a) client-action: run and short-circuit. No chip, no send.
|
|
809
809
|
if (kind === "action") {
|
|
810
|
-
const args = this
|
|
811
|
-
this
|
|
810
|
+
const args = this.#deriveArgs(this.#query);
|
|
811
|
+
this.#stripQuery();
|
|
812
812
|
this.close();
|
|
813
|
-
this
|
|
814
|
-
this
|
|
813
|
+
this.#runAction(item, args);
|
|
814
|
+
this.#opts.emit?.("command", {
|
|
815
815
|
sourceId: source.id,
|
|
816
816
|
itemId: item.id,
|
|
817
817
|
kind: "action",
|
|
@@ -823,11 +823,11 @@ export class ContextMentionController {
|
|
|
823
823
|
|
|
824
824
|
// (b) prompt-macro: write resolved text into the composer, optional submit.
|
|
825
825
|
if (kind === "prompt") {
|
|
826
|
-
const args = this
|
|
827
|
-
const target = this
|
|
826
|
+
const args = this.#deriveArgs(this.#query);
|
|
827
|
+
const target = this.#captureStripTarget();
|
|
828
828
|
this.close();
|
|
829
|
-
void this
|
|
830
|
-
this
|
|
829
|
+
void this.#runPromptMacro(source, item, args, target);
|
|
830
|
+
this.#opts.emit?.("command", {
|
|
831
831
|
sourceId: source.id,
|
|
832
832
|
itemId: item.id,
|
|
833
833
|
kind: "prompt",
|
|
@@ -840,15 +840,15 @@ export class ContextMentionController {
|
|
|
840
840
|
// and track its resolve by composer id. Chip display: go through the manager
|
|
841
841
|
// (chip row + resolve). Server commands never reach here — they inline-complete
|
|
842
842
|
// above.
|
|
843
|
-
if (this
|
|
844
|
-
this
|
|
843
|
+
if (this.#isInlineDisplay()) {
|
|
844
|
+
this.#commitInlineMention(source, item);
|
|
845
845
|
return;
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
-
const ok = this
|
|
848
|
+
const ok = this.#opts.onSelect(source, item, "");
|
|
849
849
|
if (ok) {
|
|
850
|
-
this
|
|
851
|
-
this
|
|
850
|
+
this.#stripQuery();
|
|
851
|
+
this.#opts.emit?.("selected", {
|
|
852
852
|
sourceId: source.id,
|
|
853
853
|
itemId: item.id,
|
|
854
854
|
label: item.label,
|
|
@@ -859,10 +859,10 @@ export class ContextMentionController {
|
|
|
859
859
|
}
|
|
860
860
|
|
|
861
861
|
/** True when `@` mentions render as inline tokens (contenteditable composer). */
|
|
862
|
-
|
|
862
|
+
#isInlineDisplay(): boolean {
|
|
863
863
|
return (
|
|
864
|
-
this
|
|
865
|
-
!!this
|
|
864
|
+
this.#opts.mentionConfig.display === "inline" &&
|
|
865
|
+
!!this.#opts.onInsertMention &&
|
|
866
866
|
!!this.input.insertMentionAtTrigger
|
|
867
867
|
);
|
|
868
868
|
}
|
|
@@ -873,24 +873,24 @@ export class ContextMentionController {
|
|
|
873
873
|
* keyed by the returned composer id. The limit is gated BEFORE insertion so a
|
|
874
874
|
* rejected pick never leaves a stray token.
|
|
875
875
|
*/
|
|
876
|
-
|
|
876
|
+
#commitInlineMention(
|
|
877
877
|
source: AgentWidgetContextMentionSource,
|
|
878
878
|
item: AgentWidgetContextMentionItem
|
|
879
879
|
): void {
|
|
880
880
|
// Limit gate BEFORE insertion (fires the manager's rejection hooks) so a
|
|
881
881
|
// rejected pick never leaves a stray token.
|
|
882
|
-
if (this
|
|
882
|
+
if (this.#opts.admitMention && !this.#opts.admitMention(source, item)) {
|
|
883
883
|
this.close();
|
|
884
884
|
this.input.focus();
|
|
885
885
|
return;
|
|
886
886
|
}
|
|
887
887
|
const ref = refFromItem(source, item);
|
|
888
|
-
const id = this
|
|
889
|
-
? this.input.insertMentionAtTrigger!(ref, this
|
|
888
|
+
const id = this.#triggerMatch
|
|
889
|
+
? this.input.insertMentionAtTrigger!(ref, this.#triggerMatch)
|
|
890
890
|
: (this.input.insertMentionAtSelection?.(ref) ?? null);
|
|
891
891
|
if (id) {
|
|
892
|
-
this
|
|
893
|
-
this
|
|
892
|
+
this.#opts.onInsertMention!(id, source, item, "");
|
|
893
|
+
this.#opts.emit?.("selected", {
|
|
894
894
|
sourceId: source.id,
|
|
895
895
|
itemId: item.id,
|
|
896
896
|
label: item.label,
|
|
@@ -900,15 +900,15 @@ export class ContextMentionController {
|
|
|
900
900
|
// change between parse and commit (an IME/edit race). Fire the same
|
|
901
901
|
// rejection path chip-mode failures use so hosts/tests can react rather than
|
|
902
902
|
// the typed `@query` being silently stranded.
|
|
903
|
-
this
|
|
904
|
-
this
|
|
903
|
+
this.#opts.mentionConfig.onMentionRejected?.(item, "stale");
|
|
904
|
+
this.#opts.emit?.("rejected", {
|
|
905
905
|
sourceId: source.id,
|
|
906
906
|
itemId: item.id,
|
|
907
907
|
reason: "stale",
|
|
908
908
|
});
|
|
909
909
|
}
|
|
910
910
|
// The `@query` range became the token (typed path); nothing left to strip.
|
|
911
|
-
this
|
|
911
|
+
this.#triggerMatch = null;
|
|
912
912
|
this.close();
|
|
913
913
|
this.input.focus();
|
|
914
914
|
}
|
|
@@ -920,18 +920,18 @@ export class ContextMentionController {
|
|
|
920
920
|
* submit with the typed args. The trailing space + `isInlineArgTail` keep the
|
|
921
921
|
* menu from reopening while the argument is typed.
|
|
922
922
|
*/
|
|
923
|
-
|
|
923
|
+
#completeCommandInline(
|
|
924
924
|
source: AgentWidgetContextMentionSource,
|
|
925
925
|
item: AgentWidgetContextMentionItem
|
|
926
926
|
): void {
|
|
927
|
-
const insert = `${this
|
|
927
|
+
const insert = `${this.#activeChannel.trigger}${item.label} `;
|
|
928
928
|
const caret = this.input.getSelection().start;
|
|
929
929
|
// Typed path: replace the `<trigger><query>` span. Picker path (button open,
|
|
930
930
|
// no trigger char present): insert at the caret. Line-start commands only
|
|
931
931
|
// dispatch when they lead a line, so a mid-line picker insert degrades to
|
|
932
932
|
// literal text — acceptable for the rarely-used slash button. Both endpoints
|
|
933
933
|
// are LOGICAL offsets, so the edit must run in logical space (see below).
|
|
934
|
-
const start = this
|
|
934
|
+
const start = this.#triggerMatch ? this.#triggerMatch.triggerIndex : caret;
|
|
935
935
|
if (this.input.replaceLogicalRange) {
|
|
936
936
|
// Inline: splice at the document level so tokens elsewhere in the line
|
|
937
937
|
// survive (a display-string slice would misalign against `@Label` and
|
|
@@ -945,9 +945,9 @@ export class ContextMentionController {
|
|
|
945
945
|
start + insert.length
|
|
946
946
|
);
|
|
947
947
|
}
|
|
948
|
-
this
|
|
948
|
+
this.#triggerMatch = null;
|
|
949
949
|
this.close();
|
|
950
|
-
this
|
|
950
|
+
this.#opts.emit?.("command", {
|
|
951
951
|
sourceId: source.id,
|
|
952
952
|
itemId: item.id,
|
|
953
953
|
kind: item.command ?? "prompt",
|
|
@@ -963,7 +963,7 @@ export class ContextMentionController {
|
|
|
963
963
|
* followed by argument text (`"lookup "`, `"lookup 1042"`). Selecting is done;
|
|
964
964
|
* the menu closes so the list doesn't reappear while the argument is typed.
|
|
965
965
|
*/
|
|
966
|
-
|
|
966
|
+
#isInlineArgTail(
|
|
967
967
|
channel: NormalizedMentionChannel,
|
|
968
968
|
query: string
|
|
969
969
|
): boolean {
|
|
@@ -971,7 +971,7 @@ export class ContextMentionController {
|
|
|
971
971
|
if (!name || query.length <= name.length) return false;
|
|
972
972
|
return channel.sources.some((s) => {
|
|
973
973
|
const item = s.matchCommand?.(name);
|
|
974
|
-
return !!item && this
|
|
974
|
+
return !!item && this.#isInlineCommand(item);
|
|
975
975
|
});
|
|
976
976
|
}
|
|
977
977
|
|
|
@@ -982,12 +982,12 @@ export class ContextMentionController {
|
|
|
982
982
|
* commands match. Returns the source, item, and the argument text after the
|
|
983
983
|
* name. Handles menu-selected and hand-typed commands uniformly.
|
|
984
984
|
*/
|
|
985
|
-
|
|
985
|
+
#matchInlineCommand(text: string): {
|
|
986
986
|
source: AgentWidgetContextMentionSource;
|
|
987
987
|
item: AgentWidgetContextMentionItem;
|
|
988
988
|
args: string;
|
|
989
989
|
} | null {
|
|
990
|
-
for (const channel of this
|
|
990
|
+
for (const channel of this.#channels) {
|
|
991
991
|
if (!channel.trigger) continue;
|
|
992
992
|
// Match the live trigger parser: a line-start command may lead ANY line
|
|
993
993
|
// (not just the first); input-start only the very first; anywhere the whole
|
|
@@ -1004,7 +1004,7 @@ export class ContextMentionController {
|
|
|
1004
1004
|
if (!name) continue;
|
|
1005
1005
|
for (const source of channel.sources) {
|
|
1006
1006
|
const item = source.matchCommand?.(name);
|
|
1007
|
-
if (item && this
|
|
1007
|
+
if (item && this.#isInlineCommand(item)) return { source, item, args };
|
|
1008
1008
|
}
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
@@ -1018,11 +1018,11 @@ export class ContextMentionController {
|
|
|
1018
1018
|
* resolved `context`. Returns null when `text` isn't an inline command.
|
|
1019
1019
|
*/
|
|
1020
1020
|
async dispatchInlineCommand(text: string): Promise<InlineCommandResult | null> {
|
|
1021
|
-
const match = this
|
|
1021
|
+
const match = this.#matchInlineCommand(text);
|
|
1022
1022
|
if (!match) return null;
|
|
1023
1023
|
const { source, item, args } = match;
|
|
1024
1024
|
const kind = item.command ?? "prompt";
|
|
1025
|
-
this
|
|
1025
|
+
this.#opts.emit?.("command", {
|
|
1026
1026
|
sourceId: source.id,
|
|
1027
1027
|
itemId: item.id,
|
|
1028
1028
|
kind,
|
|
@@ -1030,14 +1030,14 @@ export class ContextMentionController {
|
|
|
1030
1030
|
});
|
|
1031
1031
|
|
|
1032
1032
|
if (kind === "action") {
|
|
1033
|
-
this
|
|
1033
|
+
this.#runAction(item, args);
|
|
1034
1034
|
return { kind: "action" };
|
|
1035
1035
|
}
|
|
1036
1036
|
|
|
1037
1037
|
if (kind === "prompt") {
|
|
1038
1038
|
let payload;
|
|
1039
1039
|
try {
|
|
1040
|
-
payload = await source.resolve(item, this
|
|
1040
|
+
payload = await source.resolve(item, this.#resolveContext(args, text));
|
|
1041
1041
|
} catch (error) {
|
|
1042
1042
|
if (typeof console !== "undefined") {
|
|
1043
1043
|
console.warn("[Persona] inline prompt command resolve failed", error);
|
|
@@ -1049,7 +1049,7 @@ export class ContextMentionController {
|
|
|
1049
1049
|
|
|
1050
1050
|
// server: resolve at submit; refs empty (no chip), context namespaced.
|
|
1051
1051
|
const finalize = async (): Promise<MentionSubmitBundle> => {
|
|
1052
|
-
const payload = await source.resolve(item, this
|
|
1052
|
+
const payload = await source.resolve(item, this.#resolveContext(args, text));
|
|
1053
1053
|
const context: Record<string, Record<string, unknown>> = {};
|
|
1054
1054
|
if (payload.context) context[source.id] = { [item.id]: payload.context };
|
|
1055
1055
|
return {
|
|
@@ -1061,10 +1061,10 @@ export class ContextMentionController {
|
|
|
1061
1061
|
return { kind: "server", mentions: { refs: [], finalize } };
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
-
|
|
1064
|
+
#resolveContext(args: string, composerText: string) {
|
|
1065
1065
|
return {
|
|
1066
|
-
messages: this
|
|
1067
|
-
config: this
|
|
1066
|
+
messages: this.#opts.getMessages(),
|
|
1067
|
+
config: this.#opts.getConfig(),
|
|
1068
1068
|
composerText,
|
|
1069
1069
|
args,
|
|
1070
1070
|
signal: new AbortController().signal,
|
|
@@ -1072,14 +1072,14 @@ export class ContextMentionController {
|
|
|
1072
1072
|
}
|
|
1073
1073
|
|
|
1074
1074
|
/** Run a `command:"action"` handler, guarding against throws. */
|
|
1075
|
-
|
|
1075
|
+
#runAction(item: AgentWidgetContextMentionItem, args: string): void {
|
|
1076
1076
|
if (!item.action) return;
|
|
1077
1077
|
try {
|
|
1078
1078
|
void Promise.resolve(
|
|
1079
1079
|
item.action({
|
|
1080
1080
|
args,
|
|
1081
|
-
config: this
|
|
1082
|
-
messages: this
|
|
1081
|
+
config: this.#opts.getConfig(),
|
|
1082
|
+
messages: this.#opts.getMessages(),
|
|
1083
1083
|
composer: this.input,
|
|
1084
1084
|
})
|
|
1085
1085
|
).catch((error) => {
|
|
@@ -1099,21 +1099,21 @@ export class ContextMentionController {
|
|
|
1099
1099
|
* range `[triggerIndex, caret)` to rewrite, plus the display `value` at select
|
|
1100
1100
|
* time for the textarea fallback (where DISPLAY === LOGICAL).
|
|
1101
1101
|
*/
|
|
1102
|
-
|
|
1102
|
+
#captureStripTarget(): {
|
|
1103
1103
|
value: string;
|
|
1104
1104
|
start: number;
|
|
1105
1105
|
end: number;
|
|
1106
1106
|
} | null {
|
|
1107
|
-
if (!this
|
|
1107
|
+
if (!this.#triggerMatch) return null;
|
|
1108
1108
|
return {
|
|
1109
1109
|
value: this.input.getValue(),
|
|
1110
|
-
start: this
|
|
1110
|
+
start: this.#triggerMatch.triggerIndex,
|
|
1111
1111
|
end: this.input.getSelection().start,
|
|
1112
1112
|
};
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
1115
|
/** Resolve a `command:"prompt"` macro and write its text into the composer. */
|
|
1116
|
-
|
|
1116
|
+
async #runPromptMacro(
|
|
1117
1117
|
source: AgentWidgetContextMentionSource,
|
|
1118
1118
|
item: AgentWidgetContextMentionItem,
|
|
1119
1119
|
args: string,
|
|
@@ -1123,8 +1123,8 @@ export class ContextMentionController {
|
|
|
1123
1123
|
let payload;
|
|
1124
1124
|
try {
|
|
1125
1125
|
payload = await source.resolve(item, {
|
|
1126
|
-
messages: this
|
|
1127
|
-
config: this
|
|
1126
|
+
messages: this.#opts.getMessages(),
|
|
1127
|
+
config: this.#opts.getConfig(),
|
|
1128
1128
|
composerText: composer.getValue(),
|
|
1129
1129
|
args,
|
|
1130
1130
|
signal: new AbortController().signal,
|
|
@@ -1154,30 +1154,30 @@ export class ContextMentionController {
|
|
|
1154
1154
|
if (item.submitOnSelect) composer.submit();
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
|
-
|
|
1158
|
-
if (!this
|
|
1157
|
+
#stripQuery(): void {
|
|
1158
|
+
if (!this.#triggerMatch) return;
|
|
1159
1159
|
const caret = this.input.getSelection().start;
|
|
1160
1160
|
if (this.input.replaceLogicalRange) {
|
|
1161
1161
|
// Inline: remove the `@query` span in logical space, preserving every token
|
|
1162
1162
|
// outside `[triggerIndex, caret)`. A display-string strip would misalign and
|
|
1163
1163
|
// a full re-render would destroy the tokens.
|
|
1164
|
-
this.input.replaceLogicalRange(this
|
|
1164
|
+
this.input.replaceLogicalRange(this.#triggerMatch.triggerIndex, caret, "");
|
|
1165
1165
|
} else {
|
|
1166
1166
|
// Textarea: DISPLAY === LOGICAL, so the string strip is exact.
|
|
1167
|
-
const out = stripMentionQuery(this.input.getValue(), this
|
|
1167
|
+
const out = stripMentionQuery(this.input.getValue(), this.#triggerMatch, caret);
|
|
1168
1168
|
this.input.setValueWithCaret(out.value, out.caret);
|
|
1169
1169
|
}
|
|
1170
1170
|
// Auto-resize listener reacts to input; our re-parse yields null → stays closed.
|
|
1171
1171
|
this.input.dispatchInput();
|
|
1172
|
-
this
|
|
1172
|
+
this.#triggerMatch = null;
|
|
1173
1173
|
}
|
|
1174
1174
|
|
|
1175
1175
|
destroy(): void {
|
|
1176
|
-
this
|
|
1177
|
-
if (this
|
|
1178
|
-
this
|
|
1179
|
-
this
|
|
1180
|
-
this
|
|
1176
|
+
this.#disconnectComposerResize();
|
|
1177
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
1178
|
+
this.#searchAbort?.abort();
|
|
1179
|
+
this.#popover?.destroy();
|
|
1180
|
+
this.#menu.destroy();
|
|
1181
1181
|
const el = this.input.element;
|
|
1182
1182
|
el.removeAttribute("aria-haspopup");
|
|
1183
1183
|
el.removeAttribute("aria-controls");
|