@leaves615/dsh-llm-ctl 0.1.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.
@@ -0,0 +1,428 @@
1
+ /**
2
+ * DOM filtering for the model-selection popup (PRD FR3.2 / FR3.3).
3
+ *
4
+ * The module owns no state and no event listeners of its own: callers hand it
5
+ * the popup roots (or a container to search) and it reports what it hid. The
6
+ * selectors are deliberately fuzzy — the popup's hashed class prefixes change
7
+ * on every harness build, so only `[class*=...]` matching and the stable
8
+ * ARIA/role contract may be relied upon.
9
+ *
10
+ * @module dsh-llm-ctl/menu-filter
11
+ */
12
+ /** Stable id of the injected empty-state element. */
13
+ const EMPTY_STATE_ID = 'dsh-llm-ctl-empty';
14
+ /** `Node.DOCUMENT_POSITION_FOLLOWING`, inlined to avoid depending on a DOM global. */
15
+ const DOCUMENT_POSITION_FOLLOWING = 4;
16
+ /** True when an aria-label names the model-selection menu. */
17
+ function isModelMenuLabel(label) {
18
+ const value = label.toLowerCase();
19
+ return value.includes('model') || label.includes('推理等级') || value.includes('effort');
20
+ }
21
+ /** True when the node exposes `Element.matches` (avoids `instanceof` on globals). */
22
+ function isElement(node) {
23
+ return typeof node === 'object' && node !== null && typeof node.matches === 'function';
24
+ }
25
+ /** The groups container of `roots`, or undefined when the roots are malformed. */
26
+ function groupsOf(roots) {
27
+ const groups = roots?.groups;
28
+ return groups ?? undefined;
29
+ }
30
+ /** The menu element of `roots`, or undefined when the roots are malformed. */
31
+ function menuOf(roots) {
32
+ const menu = roots?.menu;
33
+ return menu ?? undefined;
34
+ }
35
+ /** The first reachable document among the candidates. */
36
+ function documentOf(...candidates) {
37
+ for (const candidate of candidates) {
38
+ const owner = candidate?.ownerDocument;
39
+ if (owner !== undefined && owner !== null)
40
+ return owner;
41
+ }
42
+ return typeof document === 'undefined' ? undefined : document;
43
+ }
44
+ /** The raw class attribute of an element, never its `className` object form. */
45
+ function classAttributeOf(element) {
46
+ return element.getAttribute('class') ?? '';
47
+ }
48
+ /**
49
+ * Resolve the container that holds the `section[role="group"]` children.
50
+ *
51
+ * The popup wraps the groups in a plain div (no `role`), optionally preceded by
52
+ * presentation wrappers such as a search-box row.
53
+ */
54
+ function findGroupsContainer(menu) {
55
+ for (const candidate of menu.querySelectorAll('div')) {
56
+ if (candidate.getAttribute('role') !== null)
57
+ continue;
58
+ if (candidate.querySelector('[role="group"]') !== null)
59
+ return candidate;
60
+ }
61
+ for (const child of Array.from(menu.children)) {
62
+ if (child.querySelector('[role="group"]') !== null)
63
+ return child;
64
+ }
65
+ return menu;
66
+ }
67
+ /**
68
+ * Find the model-selection popup inside a container.
69
+ *
70
+ * Matches `div[role="menu"]` whose aria-label mentions the model menu in either
71
+ * locale ("模型与推理等级", "Model and reasoning effort", or any label containing
72
+ * `model`/`推理等级`/`effort`). When no labelled candidate exists, an unlabelled
73
+ * menu that already contains `[role="group"]` is accepted. The first match wins.
74
+ *
75
+ * @param root Document, fragment, or element to search within.
76
+ * @returns The popup roots, or undefined when no model menu is present.
77
+ */
78
+ export function findModelMenu(root) {
79
+ try {
80
+ if (root === undefined || root === null)
81
+ return undefined;
82
+ let menu;
83
+ if (isElement(root) && root.matches('div[role="menu"]') && isModelMenuLabel(root.getAttribute('aria-label') ?? '')) {
84
+ menu = root;
85
+ }
86
+ const candidates = root.querySelectorAll('div[role="menu"]');
87
+ if (menu === undefined) {
88
+ for (const candidate of candidates) {
89
+ if (isModelMenuLabel(candidate.getAttribute('aria-label') ?? '')) {
90
+ menu = candidate;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ if (menu === undefined) {
96
+ for (const candidate of candidates) {
97
+ if ((candidate.getAttribute('aria-label') ?? '').trim() !== '')
98
+ continue;
99
+ if (candidate.querySelector('[role="group"]') === null)
100
+ continue;
101
+ menu = candidate;
102
+ break;
103
+ }
104
+ }
105
+ if (menu === undefined)
106
+ return undefined;
107
+ return { menu, groups: findGroupsContainer(menu) };
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ /** Read the provider title of one group: its labelled div, then aria-labelledby. */
114
+ function readGroupTitle(group) {
115
+ try {
116
+ const titled = group.querySelector('div[id]');
117
+ const fromTitle = titled?.textContent?.trim() ?? '';
118
+ if (fromTitle !== '')
119
+ return fromTitle;
120
+ const labelledBy = (group.getAttribute('aria-labelledby') ?? '').trim();
121
+ if (labelledBy !== '') {
122
+ const owner = group.ownerDocument;
123
+ for (const id of labelledBy.split(/\s+/)) {
124
+ const target = owner?.getElementById(id);
125
+ const text = target?.textContent?.trim() ?? '';
126
+ if (text !== '')
127
+ return text;
128
+ }
129
+ }
130
+ return (group.getAttribute('aria-label') ?? '').trim();
131
+ }
132
+ catch {
133
+ return '';
134
+ }
135
+ }
136
+ /** Read the model display name: title attribute, then `[class*="modelName"]`, then all text. */
137
+ function readRowName(row) {
138
+ try {
139
+ const title = (row.getAttribute('title') ?? '').trim();
140
+ if (title !== '')
141
+ return title;
142
+ const named = row.querySelector('[class*="modelName"]');
143
+ const fromClass = named?.textContent?.trim() ?? '';
144
+ if (fromClass !== '')
145
+ return fromClass;
146
+ return (row.textContent ?? '').trim();
147
+ }
148
+ catch {
149
+ return '';
150
+ }
151
+ }
152
+ /**
153
+ * List every model row of the popup in DOM order, including rows currently hidden.
154
+ *
155
+ * @param roots Popup roots from {@link findModelMenu}.
156
+ * @returns One entry per `[role="menuitemradio"]` inside a `section[role="group"]`.
157
+ */
158
+ export function listModelRows(roots) {
159
+ const rows = [];
160
+ const groups = groupsOf(roots);
161
+ if (groups === undefined)
162
+ return rows;
163
+ try {
164
+ for (const group of groups.querySelectorAll('section[role="group"]')) {
165
+ const providerName = readGroupTitle(group);
166
+ for (const row of group.querySelectorAll('[role="menuitemradio"]')) {
167
+ rows.push({ row, providerName, modelName: readRowName(row) });
168
+ }
169
+ }
170
+ }
171
+ catch {
172
+ // Malformed DOM: keep whatever was collected so far.
173
+ }
174
+ return rows;
175
+ }
176
+ /**
177
+ * Split one query line into provider-scoped and plain terms.
178
+ *
179
+ * @param raw Raw query text as typed by the user.
180
+ * @returns Lowercased provider terms and model terms.
181
+ */
182
+ export function parseMenuQuery(raw) {
183
+ const providerTerms = [];
184
+ const modelTerms = [];
185
+ for (const token of raw.trim().toLowerCase().split(/\s+/).filter((part) => part.length > 0)) {
186
+ const providerTerm = token.startsWith('p:') ? token.slice(2) : token.startsWith('provider:') ? token.slice(9) : undefined;
187
+ if (providerTerm !== undefined) {
188
+ if (providerTerm.length > 0)
189
+ providerTerms.push(providerTerm);
190
+ }
191
+ else {
192
+ modelTerms.push(token);
193
+ }
194
+ }
195
+ return { providerTerms, modelTerms };
196
+ }
197
+ /** True when the row matches the (already lowercased, trimmed) query. */
198
+ function matchesQuery(row, query) {
199
+ const { providerTerms, modelTerms } = parseMenuQuery(query);
200
+ const provider = row.providerName.toLowerCase();
201
+ const model = row.modelName.toLowerCase();
202
+ return providerTerms.every((term) => provider.includes(term)) && modelTerms.every((term) => model.includes(term) || provider.includes(term));
203
+ }
204
+ /**
205
+ * Apply the query and visibility predicate to every row and collapse empty groups.
206
+ *
207
+ * A row is shown only when it passes `isRowVisible` (when supplied) and matches
208
+ * `query` (when `hideUnmatched` is true). Query tokens match the model or provider
209
+ * name; a `p:` or `provider:` prefix restricts that token to the provider name.
210
+ * A group whose rows are all hidden is itself hidden; groups without rows are
211
+ * left untouched.
212
+ *
213
+ * @param roots Popup roots from {@link findModelMenu}.
214
+ * @param options Filter inputs; omit for "show everything".
215
+ * @returns Counts of shown rows, hidden rows, and hidden groups.
216
+ */
217
+ export function applyMenuFilter(roots, options = {}) {
218
+ const result = { shown: 0, hidden: 0, groupsHidden: 0 };
219
+ const groups = groupsOf(roots);
220
+ if (groups === undefined)
221
+ return result;
222
+ try {
223
+ const hideUnmatched = options?.hideUnmatched !== false;
224
+ const query = hideUnmatched ? (options?.query ?? '').trim().toLowerCase() : '';
225
+ const isRowVisible = options?.isRowVisible;
226
+ for (const group of groups.querySelectorAll('section[role="group"]')) {
227
+ const providerName = readGroupTitle(group);
228
+ const rows = group.querySelectorAll('[role="menuitemradio"]');
229
+ let visibleRows = 0;
230
+ for (const row of rows) {
231
+ const model = { row, providerName, modelName: readRowName(row) };
232
+ let visible = true;
233
+ if (typeof isRowVisible === 'function') {
234
+ try {
235
+ visible = isRowVisible(model) !== false;
236
+ }
237
+ catch {
238
+ visible = false;
239
+ }
240
+ }
241
+ if (visible && hideUnmatched && !matchesQuery(model, query))
242
+ visible = false;
243
+ row.style.display = visible ? '' : 'none';
244
+ if (visible)
245
+ visibleRows += 1;
246
+ else
247
+ result.hidden += 1;
248
+ }
249
+ result.shown += visibleRows;
250
+ if (rows.length > 0 && visibleRows === 0) {
251
+ group.style.display = 'none';
252
+ result.groupsHidden += 1;
253
+ }
254
+ else {
255
+ group.style.display = '';
256
+ }
257
+ }
258
+ }
259
+ catch {
260
+ // Malformed DOM: return the counts accumulated so far.
261
+ }
262
+ return result;
263
+ }
264
+ /**
265
+ * Clear every inline `display` this module may have written.
266
+ *
267
+ * Elements are never removed; rows and groups return to their natural layout.
268
+ *
269
+ * @param roots Popup roots from {@link findModelMenu}.
270
+ */
271
+ export function resetMenuFilter(roots) {
272
+ const groups = groupsOf(roots);
273
+ if (groups === undefined)
274
+ return;
275
+ try {
276
+ for (const group of groups.querySelectorAll('section[role="group"]')) {
277
+ group.style.display = '';
278
+ for (const row of group.querySelectorAll('[role="menuitemradio"]')) {
279
+ row.style.display = '';
280
+ delete row.dataset['llmCtlHidden'];
281
+ }
282
+ }
283
+ }
284
+ catch {
285
+ // Malformed DOM: nothing to reset.
286
+ }
287
+ }
288
+ /** True when the element itself is, or contains, a `dsh-model-search-plugin` widget. */
289
+ function containsForeignSearch(element) {
290
+ if (element.matches('[class*="dsh-model-search-container"]'))
291
+ return true;
292
+ if (element.querySelector('[class*="dsh-model-search-container"]') !== null)
293
+ return true;
294
+ if (element.matches('input') && classAttributeOf(element).includes('dsh-model-search'))
295
+ return true;
296
+ for (const input of element.querySelectorAll('input')) {
297
+ if (classAttributeOf(input).includes('dsh-model-search'))
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+ /** True when `element` precedes `other` in document order. */
303
+ function isBefore(element, other) {
304
+ if (element === other)
305
+ return false;
306
+ if (typeof element.compareDocumentPosition !== 'function')
307
+ return true;
308
+ return (element.compareDocumentPosition(other) & DOCUMENT_POSITION_FOLLOWING) !== 0;
309
+ }
310
+ /**
311
+ * Detect a search widget injected by `dsh-model-search-plugin`.
312
+ *
313
+ * A foreign widget sits before the groups container, either as its previous
314
+ * sibling or anywhere inside the menu (or the menu's parent). When one is found
315
+ * the caller must not inject a second search box (PRD FR3.2).
316
+ *
317
+ * @param roots Popup roots from {@link findModelMenu}.
318
+ * @returns True when a foreign search widget occupies the slot above the groups.
319
+ */
320
+ export function hasForeignSearchWidget(roots) {
321
+ const groups = groupsOf(roots);
322
+ const menu = menuOf(roots);
323
+ if (groups === undefined || menu === undefined)
324
+ return false;
325
+ try {
326
+ const previous = groups.previousElementSibling;
327
+ if (previous !== null && containsForeignSearch(previous))
328
+ return true;
329
+ const scopes = [menu];
330
+ const parent = menu.parentElement;
331
+ if (parent !== null)
332
+ scopes.push(parent);
333
+ const anchor = groups.querySelector('section[role="group"]') ?? groups;
334
+ for (const scope of scopes) {
335
+ const widgets = scope.querySelectorAll('[class*="dsh-model-search-container"], input[class*="dsh-model-search-input"]');
336
+ for (const widget of widgets) {
337
+ if (widget === anchor || widget.contains(anchor))
338
+ continue;
339
+ if (!isBefore(widget, anchor))
340
+ continue;
341
+ return true;
342
+ }
343
+ }
344
+ return false;
345
+ }
346
+ catch {
347
+ return false;
348
+ }
349
+ }
350
+ /** Find an already-injected empty state near the popup. */
351
+ function findEmptyState(groups, menu) {
352
+ const parent = groups?.parentElement ?? menu?.parentElement ?? null;
353
+ const local = parent?.querySelector(`#${EMPTY_STATE_ID}`);
354
+ if (local !== undefined && local !== null)
355
+ return local;
356
+ const global = documentOf(groups, menu)?.getElementById(EMPTY_STATE_ID);
357
+ return global ?? undefined;
358
+ }
359
+ /**
360
+ * Insert (or update) the "everything is filtered out" empty state above the groups.
361
+ *
362
+ * The element carries the message text plus one button that invokes `onAction`,
363
+ * so a caller can restore the hidden entries in one click (PRD FR3.3). Repeated
364
+ * calls reuse the same element and replace its text, button label, and handler.
365
+ *
366
+ * @param roots Popup roots from {@link findModelMenu}.
367
+ * @param text Message shown before the action button.
368
+ * @param actionLabel Button label, e.g. "显示 3 个隐藏项".
369
+ * @param onAction Click handler for that button.
370
+ * @returns The inserted `#dsh-llm-ctl-empty` element.
371
+ */
372
+ export function ensureEmptyState(roots, text, actionLabel, onAction) {
373
+ const groups = groupsOf(roots);
374
+ const menu = menuOf(roots);
375
+ const existing = findEmptyState(groups, menu);
376
+ const owner = documentOf(existing, groups, menu);
377
+ if (owner === undefined)
378
+ return (roots?.menu ?? roots?.groups);
379
+ let element = existing;
380
+ try {
381
+ element = element ?? owner.createElement('div');
382
+ element.id = EMPTY_STATE_ID;
383
+ element.className = 'dsh-llm-ctl-empty';
384
+ const message = owner.createElement('span');
385
+ message.className = 'dsh-llm-ctl-empty-text';
386
+ message.textContent = text;
387
+ const action = owner.createElement('button');
388
+ action.type = 'button';
389
+ action.className = 'dsh-llm-ctl-empty-action';
390
+ action.textContent = actionLabel;
391
+ action.addEventListener('click', (event) => {
392
+ event.preventDefault();
393
+ try {
394
+ onAction();
395
+ }
396
+ catch {
397
+ // A caller-supplied handler must never break the popup.
398
+ }
399
+ });
400
+ element.replaceChildren(message, action);
401
+ const parent = groups?.parentElement ?? menu?.parentElement ?? null;
402
+ if (parent !== null) {
403
+ if (groups !== undefined && groups.parentElement === parent)
404
+ parent.insertBefore(element, groups);
405
+ else if (element.parentElement !== parent)
406
+ parent.appendChild(element);
407
+ }
408
+ }
409
+ catch {
410
+ // Malformed DOM: return whatever element could be built.
411
+ }
412
+ return element ?? (roots?.menu ?? roots?.groups);
413
+ }
414
+ /**
415
+ * Remove the empty state injected by {@link ensureEmptyState}, if present.
416
+ *
417
+ * @param roots Popup roots from {@link findModelMenu}.
418
+ */
419
+ export function removeEmptyState(roots) {
420
+ try {
421
+ const groups = groupsOf(roots);
422
+ const menu = menuOf(roots);
423
+ findEmptyState(groups, menu)?.remove();
424
+ }
425
+ catch {
426
+ // Malformed DOM: nothing to remove.
427
+ }
428
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Hide-only reconciliation for menus owned by a foreign search widget.
3
+ *
4
+ * A foreign plugin (for example dsh-model-search-plugin) owns query filtering,
5
+ * group collapsing, and empty states in its menu. Rewriting every passing row's
6
+ * display would wipe its filtering on every poll tick, which is exactly the
7
+ * type-then-revert symptom. This pass only ever hides rows our own switches
8
+ * reject, and it only ever restores rows it hid itself (tracked with a data
9
+ * marker), so the two plugins compose instead of fighting.
10
+ *
11
+ * @module dsh-llm-ctl/menu-visibility
12
+ */
13
+ import type { ModelMenuRoots, ModelMenuRow } from './menu-filter.ts';
14
+ /** Outcome of one hide-only reconciliation pass. */
15
+ export interface VisibilityOnlyResult {
16
+ hidden: number;
17
+ restored: number;
18
+ }
19
+ /**
20
+ * Hide rows rejected by our own switches without touching anything else.
21
+ *
22
+ * @param roots Popup roots from findModelMenu.
23
+ * @param isRowVisible Visibility predicate; rows without a decision stay visible.
24
+ * @returns Counts of newly hidden and restored rows.
25
+ */
26
+ export declare function applyVisibilityOnly(roots: ModelMenuRoots, isRowVisible: (row: ModelMenuRow) => boolean): VisibilityOnlyResult;
@@ -0,0 +1,77 @@
1
+ /** Marker set on rows hidden by this pass, so only they are ever restored. */
2
+ const HIDDEN_MARKER = 'llmCtlHidden';
3
+ /** Group title with the same fallbacks as the menu parser. */
4
+ function groupTitleOf(group) {
5
+ try {
6
+ const titled = group.querySelector('div[id]');
7
+ if (titled !== null)
8
+ return (titled.textContent ?? '').trim();
9
+ const labelledBy = group.getAttribute('aria-labelledby');
10
+ if (labelledBy !== null && labelledBy.length > 0) {
11
+ const target = group.ownerDocument?.getElementById(labelledBy);
12
+ if (target !== null && target !== undefined)
13
+ return (target.textContent ?? '').trim();
14
+ }
15
+ return (group.getAttribute('aria-label') ?? '').trim();
16
+ }
17
+ catch {
18
+ return '';
19
+ }
20
+ }
21
+ /** Row display name with the same fallbacks as the menu parser. */
22
+ function rowNameOf(row) {
23
+ try {
24
+ const title = row.getAttribute('title');
25
+ if (title !== null && title.trim().length > 0)
26
+ return title.trim();
27
+ const named = row.querySelector('[class*="modelName"]');
28
+ if (named !== null && (named.textContent ?? '').trim().length > 0)
29
+ return (named.textContent ?? '').trim();
30
+ return (row.textContent ?? '').trim();
31
+ }
32
+ catch {
33
+ return '';
34
+ }
35
+ }
36
+ /**
37
+ * Hide rows rejected by our own switches without touching anything else.
38
+ *
39
+ * @param roots Popup roots from findModelMenu.
40
+ * @param isRowVisible Visibility predicate; rows without a decision stay visible.
41
+ * @returns Counts of newly hidden and restored rows.
42
+ */
43
+ export function applyVisibilityOnly(roots, isRowVisible) {
44
+ const result = { hidden: 0, restored: 0 };
45
+ try {
46
+ for (const group of roots.groups.querySelectorAll('section[role="group"]')) {
47
+ const providerName = groupTitleOf(group);
48
+ for (const row of group.querySelectorAll('[role="menuitemradio"]')) {
49
+ const model = { row, providerName, modelName: rowNameOf(row) };
50
+ let visible = true;
51
+ try {
52
+ visible = isRowVisible(model) !== false;
53
+ }
54
+ catch {
55
+ visible = false;
56
+ }
57
+ if (!visible) {
58
+ if (row.style.display !== 'none')
59
+ row.style.display = 'none';
60
+ if (row.dataset[HIDDEN_MARKER] !== '1') {
61
+ row.dataset[HIDDEN_MARKER] = '1';
62
+ result.hidden += 1;
63
+ }
64
+ }
65
+ else if (row.dataset[HIDDEN_MARKER] === '1') {
66
+ row.style.display = '';
67
+ delete row.dataset[HIDDEN_MARKER];
68
+ result.restored += 1;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // Malformed DOM: return the counts accumulated so far.
75
+ }
76
+ return result;
77
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * React views for the `conversation.composer.dock` queue seat.
3
+ *
4
+ * The dock is the official list slot below the composer card
5
+ * (`dsh-client-ui-conversation`, kind `list`, scope `session`, no owner
6
+ * props). Seats render as siblings inside one `display: contents` slot
7
+ * anchor: the official stats pills (`dsh-client-ui-chat` `StatsPills`,
8
+ * order 0, `.bOPqQW_root` — a centered 13px tertiary pill row) come first,
9
+ * our queue pills go last (order 1000). We cannot nest inside the stats
10
+ * seat's own root (it is owned by another plugin's component), so this panel
11
+ * mirrors its pill-row language so the two stacked rows read as one dock.
12
+ *
13
+ * Each pill is a compact trigger (glyph + count) that opens an anchored
14
+ * dialog with the full queue or cooling detail — the same interaction the
15
+ * official stats pills use. The seat returns null while the control plane is
16
+ * idle so the dock collapses to nothing.
17
+ *
18
+ * Pure view over a small state object plus callbacks: shaping and the trigger
19
+ * markup are testable without a browser. Hover/expanded styles live in the
20
+ * client half (`client-plugin.ts` `ensureDockStyles`) because inline styles
21
+ * cannot express `:hover`.
22
+ *
23
+ * @module dsh-llm-ctl/queue-dock
24
+ */
25
+ import React from 'react';
26
+ /** One queued request visible in the dock. */
27
+ export interface QueueDockWaiter {
28
+ queueId: string;
29
+ provider: string;
30
+ origin: 'loop' | 'background';
31
+ position: number;
32
+ etaMs: number;
33
+ }
34
+ /** One provider lane currently cooling down. */
35
+ export interface QueueDockLane {
36
+ provider: string;
37
+ cooldownRemainingMs: number;
38
+ }
39
+ /** Queue slice the dock renders. */
40
+ export interface QueueDockInput {
41
+ lanes: readonly QueueDockLane[];
42
+ waiters: readonly QueueDockWaiter[];
43
+ }
44
+ /** Shaped dock content: the queue and cooling detail behind the pills. */
45
+ export interface QueueDockView {
46
+ waiters: QueueDockWaiter[];
47
+ cooling: QueueDockLane[];
48
+ }
49
+ /**
50
+ * Format milliseconds as a compact human delay.
51
+ *
52
+ * @param ms - Non-negative duration in milliseconds.
53
+ * @returns Compact label such as `850ms`, `12.0s`, or `2m05s`.
54
+ */
55
+ export declare function formatMs(ms: number): string;
56
+ /**
57
+ * Shape the dock view, or undefined when the control plane is idle.
58
+ *
59
+ * @param queue - Live lanes and waiters from the state poll.
60
+ * @returns The content to render, or undefined when there is nothing to show.
61
+ */
62
+ export declare function buildQueueDockView(queue: QueueDockInput): QueueDockView | undefined;
63
+ /** Cancel-button class; hover is styled by the injected dock stylesheet. */
64
+ export declare const DOCK_CANCEL_CLASS = "dsh-llm-ctl-dock-cancel";
65
+ /** Pill-trigger class; hover/expanded styling comes from the dock stylesheet. */
66
+ export declare const DOCK_PILL_CLASS = "dsh-llm-ctl-dock-pill";
67
+ /** Dialog body: one row per waiter, each with its own cancel button. */
68
+ export declare function QueueDetails(props: {
69
+ waiters: QueueDockWaiter[];
70
+ onCancel: (queueId: string) => void;
71
+ }): React.ReactElement;
72
+ /** Dialog body: one row per cooling lane. */
73
+ export declare function CoolingDetails(props: {
74
+ lanes: QueueDockLane[];
75
+ }): React.ReactElement;
76
+ /**
77
+ * Queue/cooldown pills for the composer dock.
78
+ *
79
+ * @param props - Shaped dock view plus the cancel callback.
80
+ * @returns The rendered pill row.
81
+ */
82
+ export declare function QueueDockPanel(props: {
83
+ view: QueueDockView;
84
+ onCancel: (queueId: string) => void;
85
+ }): React.ReactElement;