@meshmakers/shared-ui 3.4.1480 → 3.5.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.
Files changed (56) hide show
  1. package/README.md +48 -0
  2. package/assets/octobot/lg/octo_blink.webp +0 -0
  3. package/assets/octobot/lg/octo_blink_still.png +0 -0
  4. package/assets/octobot/lg/octo_idle.webp +0 -0
  5. package/assets/octobot/lg/octo_idle_still.png +0 -0
  6. package/assets/octobot/lg/octo_look.webp +0 -0
  7. package/assets/octobot/lg/octo_look_still.png +0 -0
  8. package/assets/octobot/lg/octo_swim.webp +0 -0
  9. package/assets/octobot/lg/octo_swim_still.png +0 -0
  10. package/assets/octobot/lg/octo_thinking.webp +0 -0
  11. package/assets/octobot/lg/octo_thinking_still.png +0 -0
  12. package/assets/octobot/lg/octo_wave.webp +0 -0
  13. package/assets/octobot/lg/octo_wave_still.png +0 -0
  14. package/assets/octobot/md/octo_blink.webp +0 -0
  15. package/assets/octobot/md/octo_blink_still.png +0 -0
  16. package/assets/octobot/md/octo_idle.webp +0 -0
  17. package/assets/octobot/md/octo_idle_still.png +0 -0
  18. package/assets/octobot/md/octo_look.webp +0 -0
  19. package/assets/octobot/md/octo_look_still.png +0 -0
  20. package/assets/octobot/md/octo_swim.webp +0 -0
  21. package/assets/octobot/md/octo_swim_still.png +0 -0
  22. package/assets/octobot/md/octo_thinking.webp +0 -0
  23. package/assets/octobot/md/octo_thinking_still.png +0 -0
  24. package/assets/octobot/md/octo_wave.webp +0 -0
  25. package/assets/octobot/md/octo_wave_still.png +0 -0
  26. package/assets/octobot/sm/octo_blink.webp +0 -0
  27. package/assets/octobot/sm/octo_blink_still.png +0 -0
  28. package/assets/octobot/sm/octo_idle.webp +0 -0
  29. package/assets/octobot/sm/octo_idle_still.png +0 -0
  30. package/assets/octobot/sm/octo_look.webp +0 -0
  31. package/assets/octobot/sm/octo_look_still.png +0 -0
  32. package/assets/octobot/sm/octo_swim.webp +0 -0
  33. package/assets/octobot/sm/octo_swim_still.png +0 -0
  34. package/assets/octobot/sm/octo_thinking.webp +0 -0
  35. package/assets/octobot/sm/octo_thinking_still.png +0 -0
  36. package/assets/octobot/sm/octo_wave.webp +0 -0
  37. package/assets/octobot/sm/octo_wave_still.png +0 -0
  38. package/fesm2022/meshmakers-shared-ui-assistant-core.mjs +940 -0
  39. package/fesm2022/meshmakers-shared-ui-assistant-core.mjs.map +1 -0
  40. package/fesm2022/meshmakers-shared-ui-assistant.mjs +819 -0
  41. package/fesm2022/meshmakers-shared-ui-assistant.mjs.map +1 -0
  42. package/fesm2022/meshmakers-shared-ui-command-palette.mjs +817 -0
  43. package/fesm2022/meshmakers-shared-ui-command-palette.mjs.map +1 -0
  44. package/fesm2022/meshmakers-shared-ui-shell.mjs +1738 -0
  45. package/fesm2022/meshmakers-shared-ui-shell.mjs.map +1 -0
  46. package/fesm2022/meshmakers-shared-ui-testing.mjs +92 -0
  47. package/fesm2022/meshmakers-shared-ui-testing.mjs.map +1 -0
  48. package/fesm2022/meshmakers-shared-ui.mjs +1550 -203
  49. package/fesm2022/meshmakers-shared-ui.mjs.map +1 -1
  50. package/package.json +28 -2
  51. package/types/meshmakers-shared-ui-assistant-core.d.ts +732 -0
  52. package/types/meshmakers-shared-ui-assistant.d.ts +258 -0
  53. package/types/meshmakers-shared-ui-command-palette.d.ts +398 -0
  54. package/types/meshmakers-shared-ui-shell.d.ts +939 -0
  55. package/types/meshmakers-shared-ui-testing.d.ts +41 -0
  56. package/types/meshmakers-shared-ui.d.ts +1004 -154
@@ -0,0 +1,819 @@
1
+ import { assistantMessages, ASSISTANT_SLASH_COMMANDS, selectAssistantAttachments, formatAssistantMessage, proposalDecisionLabel, ASSISTANT_TOOL_CALL_COMPONENT, AssistantService, ASSISTANT_PANEL_OPTIONS, DEFAULT_ASSISTANT_PANEL_STORAGE_KEY, DEFAULT_ASSISTANT_PANEL_MIN_WIDTH, DEFAULT_ASSISTANT_PANEL_MAX_WIDTH } from '@meshmakers/shared-ui/assistant-core';
2
+ export { ASSISTANT_ENABLED, ASSISTANT_MESSAGES, ASSISTANT_NOT_CONNECTED_MESSAGE, ASSISTANT_PAGE_CONTEXT, ASSISTANT_PANEL_OPTIONS, ASSISTANT_SLASH_COMMANDS, ASSISTANT_TOOL_CALL_COMPONENT, ASSISTANT_TRANSPORT, AssistantExplainDirective, AssistantService, AssistantToolCallRowComponent, DEFAULT_ASSISTANT_MESSAGES, DEFAULT_ASSISTANT_PANEL_MAX_WIDTH, DEFAULT_ASSISTANT_PANEL_MIN_WIDTH, DEFAULT_ASSISTANT_PANEL_STORAGE_KEY, DEFAULT_ASSISTANT_SLASH_COMMANDS, FakeAssistantTransport, IfAssistantDirective, NotConnectedAssistantTransport, assistantFileAccepted, assistantHotkey, deriveAssistantContext, formatAssistantFileSize, formatAssistantMessage, proposalDecisionLabel, resolveAssistantMessages, selectAssistantAttachments } from '@meshmakers/shared-ui/assistant-core';
3
+ import * as i0 from '@angular/core';
4
+ import { Pipe, model, input, output, viewChild, signal, computed, inject, ChangeDetectionStrategy, Component, ElementRef, effect, Injector, DestroyRef, afterNextRender, untracked } from '@angular/core';
5
+ import { Marked } from 'marked';
6
+ import { SVGIconComponent } from '@progress/kendo-angular-icons';
7
+ import { xIcon, paperclipIcon, plusIcon, clockArrowRotateIcon, trashIcon } from '@progress/kendo-svg-icons';
8
+ import { NgComponentOutlet } from '@angular/common';
9
+ import { OctoBotComponent } from '@meshmakers/shared-ui';
10
+
11
+ /** Protocols an assistant link may use; everything else (javascript:, data:, vbscript: …) renders as text. */
12
+ const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
13
+ function escapeHtml(value) {
14
+ return value
15
+ .replace(/&/g, '&')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;')
19
+ .replace(/'/g, '&#39;');
20
+ }
21
+ /** Classifies a link target: external (http/https/mailto), relative (same origin) or unsafe. */
22
+ function classifyAssistantHref(href) {
23
+ const value = (href ?? '').trim();
24
+ if (!value) {
25
+ return 'unsafe';
26
+ }
27
+ // Strip control characters and whitespace browsers ignore inside a scheme ("java\nscript:").
28
+ // eslint-disable-next-line no-control-regex
29
+ const compact = value.replace(/[\u0000- \u007f]/g, '');
30
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(compact);
31
+ if (scheme) {
32
+ return SAFE_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`) ? 'external' : 'unsafe';
33
+ }
34
+ // Protocol-relative URLs leave the origin. Browsers treat a backslash like "/" in http(s)
35
+ // URLs, so "/\evil.example" and "\\evil.example" leave it as well.
36
+ if (compact.replace(/\\/g, '/').startsWith('//')) {
37
+ return 'unsafe';
38
+ }
39
+ return 'relative';
40
+ }
41
+ function safeLink(href, labelHtml, title) {
42
+ const kind = classifyAssistantHref(href);
43
+ if (kind === 'unsafe') {
44
+ return labelHtml;
45
+ }
46
+ const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
47
+ const external = kind === 'external' ? ' target="_blank" rel="noopener noreferrer"' : '';
48
+ return `<a href="${escapeHtml(href.trim())}"${titleAttr}${external}>${labelHtml}</a>`;
49
+ }
50
+ /**
51
+ * Markdown renderer for assistant turns. Model output is untrusted, so:
52
+ * raw HTML is shown as text, images never load (they become their alt text,
53
+ * linked when the source is a safe URL), links only keep http(s)/mailto and
54
+ * relative targets — external ones open in a new tab with
55
+ * `rel="noopener noreferrer"` — and anything else renders as its label.
56
+ * The result is bound with `[innerHTML]`, so Angular's sanitizer runs as a
57
+ * second layer.
58
+ */
59
+ const assistantMarked = new Marked({
60
+ async: false,
61
+ gfm: true,
62
+ renderer: {
63
+ html({ text }) {
64
+ return escapeHtml(text);
65
+ },
66
+ link(token) {
67
+ return safeLink(token.href, this.parser.parseInline(token.tokens), token.title);
68
+ },
69
+ image({ href, text }) {
70
+ const label = escapeHtml(text || 'image');
71
+ return classifyAssistantHref(href) === 'unsafe' ? label : safeLink(href, label);
72
+ }
73
+ }
74
+ });
75
+ /** Renders untrusted assistant markdown to HTML with the rules above. */
76
+ function renderAssistantMarkdown(markdown) {
77
+ return assistantMarked.parse(markdown ?? '', { async: false });
78
+ }
79
+ /** `{{ text | assistantMarkdown }}` for `[innerHTML]` bindings. */
80
+ class AssistantMarkdownPipe {
81
+ transform(markdown) {
82
+ return renderAssistantMarkdown(markdown);
83
+ }
84
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
85
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.5", ngImport: i0, type: AssistantMarkdownPipe, isStandalone: true, name: "assistantMarkdown" });
86
+ }
87
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantMarkdownPipe, decorators: [{
88
+ type: Pipe,
89
+ args: [{ name: 'assistantMarkdown' }]
90
+ }] });
91
+
92
+ let nextComposerId = 0;
93
+ /**
94
+ * Composer (ui-concept §5.3): multi-line, Enter sends, Shift+Enter is a newline,
95
+ * typing `/` at the start lists the slash commands (`ASSISTANT_SLASH_COMMANDS`, by default
96
+ * `/explain`, `/query`, `/summarise`, `/draft-pipeline`). The suggestion list follows the combobox
97
+ * pattern: focus stays in the textarea, ↑/↓ move, Enter/Tab pick, Esc closes the
98
+ * list (and only then lets Esc reach the panel).
99
+ *
100
+ * With {@link attachments} set (the transport accepts files, AB#5621) an "Attach file" button opens
101
+ * a file picker limited to `accept`; files are checked against `maxFiles` / `maxFileSizeBytes`,
102
+ * rejected ones are announced in an alert, attached ones are listed with a remove button, and a
103
+ * message may then be sent without text. Without it there is no attachment UI.
104
+ */
105
+ class AssistantComposerComponent {
106
+ /** The composer text (two-way). */
107
+ text = model('', /* @ts-ignore */
108
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
109
+ /** Whether Enter may send; false while the transport is not ready. */
110
+ canSend = input(false, /* @ts-ignore */
111
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
112
+ /** A turn is running: the send button becomes a stop button. */
113
+ sending = input(false, /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []));
115
+ /** Accepted attachments; `null` (default) = no attachment UI. */
116
+ attachments = input(null, /* @ts-ignore */
117
+ ...(ngDevMode ? [{ debugName: "attachments" }] : /* istanbul ignore next */ []));
118
+ /** The attached files (two-way); sent with the next message by the host. */
119
+ files = model([], /* @ts-ignore */
120
+ ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
121
+ send = output();
122
+ stop = output();
123
+ /** Translations; members left out fall back to {@link ASSISTANT_MESSAGES}, then English. */
124
+ messages = input(null, /* @ts-ignore */
125
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
126
+ m = assistantMessages(this.messages);
127
+ textarea = viewChild.required('input', /* @ts-ignore */
128
+ ...(ngDevMode ? [{ debugName: "textarea" }] : /* istanbul ignore next */ []));
129
+ fileInput = viewChild('fileInput', /* @ts-ignore */
130
+ ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
131
+ /** Messages about rejected files of the last pick. */
132
+ attachmentErrors = signal([], /* @ts-ignore */
133
+ ...(ngDevMode ? [{ debugName: "attachmentErrors" }] : /* istanbul ignore next */ []));
134
+ /** Something to send: text, or (with attachments) at least one file. */
135
+ hasContent = computed(() => this.text().trim().length > 0 || (!!this.attachments() && this.files().length > 0), /* @ts-ignore */
136
+ ...(ngDevMode ? [{ debugName: "hasContent" }] : /* istanbul ignore next */ []));
137
+ multiple = computed(() => (this.attachments()?.maxFiles ?? 1) > 1, /* @ts-ignore */
138
+ ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
139
+ xIcon = xIcon;
140
+ paperclipIcon = paperclipIcon;
141
+ commands = inject(ASSISTANT_SLASH_COMMANDS);
142
+ listId = `assistant-slash-${nextComposerId++}`;
143
+ activeIndex = signal(0, /* @ts-ignore */
144
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
145
+ dismissedFor = signal(null, /* @ts-ignore */
146
+ ...(ngDevMode ? [{ debugName: "dismissedFor" }] : /* istanbul ignore next */ []));
147
+ /** Slash commands matching what was typed, while the text is a bare `/word`. */
148
+ suggestions = computed(() => {
149
+ const text = this.text();
150
+ if (!/^\/[\w-]*$/.test(text) || this.dismissedFor() === text) {
151
+ return [];
152
+ }
153
+ return this.commands.filter(command => command.command.startsWith(text.toLowerCase()));
154
+ }, /* @ts-ignore */
155
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
156
+ activeOptionId = computed(() => this.suggestions().length ? `${this.listId}-${Math.min(this.activeIndex(), this.suggestions().length - 1)}` : null, /* @ts-ignore */
157
+ ...(ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []));
158
+ /** Focuses the textarea with the caret at the end (entry points prefill it). */
159
+ focus() {
160
+ const element = this.textarea().nativeElement;
161
+ element.focus();
162
+ element.setSelectionRange(element.value.length, element.value.length);
163
+ }
164
+ onInput(value) {
165
+ this.text.set(value);
166
+ this.activeIndex.set(0);
167
+ }
168
+ onKeydown(event) {
169
+ if (event.isComposing) {
170
+ return;
171
+ }
172
+ const suggestions = this.suggestions();
173
+ if (suggestions.length) {
174
+ switch (event.key) {
175
+ case 'ArrowDown':
176
+ event.preventDefault();
177
+ this.activeIndex.update(index => (index + 1) % suggestions.length);
178
+ return;
179
+ case 'ArrowUp':
180
+ event.preventDefault();
181
+ this.activeIndex.update(index => (index - 1 + suggestions.length) % suggestions.length);
182
+ return;
183
+ case 'Enter':
184
+ case 'Tab':
185
+ if (!event.shiftKey) {
186
+ event.preventDefault();
187
+ this.pick(suggestions[Math.min(this.activeIndex(), suggestions.length - 1)]);
188
+ return;
189
+ }
190
+ break;
191
+ case 'Escape':
192
+ event.preventDefault();
193
+ event.stopPropagation();
194
+ this.dismissedFor.set(this.text());
195
+ return;
196
+ }
197
+ }
198
+ if (event.key === 'Enter' && !event.shiftKey) {
199
+ event.preventDefault();
200
+ this.submit();
201
+ }
202
+ }
203
+ pick(command) {
204
+ this.text.set(`${command.command} `);
205
+ this.activeIndex.set(0);
206
+ this.focus();
207
+ }
208
+ submit() {
209
+ if (this.canSend() && this.hasContent()) {
210
+ this.attachmentErrors.set([]);
211
+ this.send.emit(this.text());
212
+ }
213
+ }
214
+ openFilePicker() {
215
+ this.fileInput()?.nativeElement.click();
216
+ }
217
+ onFilesPicked(input) {
218
+ const options = this.attachments();
219
+ const picked = Array.from(input.files ?? []);
220
+ // Reset so picking the same file again fires `change`.
221
+ input.value = '';
222
+ if (!options || !picked.length) {
223
+ return;
224
+ }
225
+ const selection = selectAssistantAttachments(this.files(), picked, options, this.m());
226
+ this.files.set(selection.files);
227
+ this.attachmentErrors.set(selection.errors);
228
+ }
229
+ removeFile(index) {
230
+ this.files.update(files => files.filter((_, i) => i !== index));
231
+ this.attachmentErrors.set([]);
232
+ this.focus();
233
+ }
234
+ format(message, values) {
235
+ return formatAssistantMessage(message, values);
236
+ }
237
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
238
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: AssistantComposerComponent, isStandalone: true, selector: "mm-assistant-composer", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, canSend: { classPropertyName: "canSend", publicName: "canSend", isSignal: true, isRequired: false, transformFunction: null }, sending: { classPropertyName: "sending", publicName: "sending", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { text: "textChange", files: "filesChange", send: "send", stop: "stop" }, viewQueries: [{ propertyName: "textarea", first: true, predicate: ["input"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"composer\">\n @if (suggestions().length) {\n <ul class=\"suggestions\" role=\"listbox\" [id]=\"listId\" [attr.aria-label]=\"m().commandsLabel\">\n @for (command of suggestions(); track command.command; let i = $index) {\n <li role=\"option\" [id]=\"listId + '-' + i\" [attr.aria-selected]=\"activeOptionId() === listId + '-' + i\"\n (mousedown)=\"$event.preventDefault()\" (click)=\"pick(command)\">\n <span class=\"cmd\">{{ command.command }}</span>\n <span class=\"cmd-desc\">{{ command.description }}</span>\n </li>\n }\n </ul>\n }\n @if (attachments(); as options) {\n @if (files().length) {\n <ul class=\"files\" [attr.aria-label]=\"m().attachmentsLabel\">\n @for (file of files(); track $index; let i = $index) {\n <li class=\"file\">\n <span class=\"file-name\" [title]=\"file.name\">{{ file.name }}</span>\n <button type=\"button\" [attr.aria-label]=\"format(m().removeAttachment, { name: file.name })\" [title]=\"format(m().removeAttachment, { name: file.name })\" (click)=\"removeFile(i)\"><kendo-svg-icon [icon]=\"xIcon\" size=\"small\"></kendo-svg-icon></button>\n </li>\n }\n </ul>\n }\n @if (attachmentErrors().length) {\n <div class=\"file-errors\" role=\"alert\">\n @for (error of attachmentErrors(); track $index) {\n <p>{{ error }}</p>\n }\n </div>\n }\n <input #fileInput class=\"file-input\" type=\"file\" tabindex=\"-1\" aria-hidden=\"true\"\n [attr.accept]=\"options.accept || null\" [multiple]=\"multiple()\"\n (change)=\"onFilesPicked($any($event.target))\">\n }\n <div class=\"box\">\n @if (attachments()) {\n <button type=\"button\" class=\"attach\" [attr.aria-label]=\"m().attachFile\" [title]=\"m().attachFile\" (click)=\"openFilePicker()\">\n <kendo-svg-icon [icon]=\"paperclipIcon\"></kendo-svg-icon>\n </button>\n }\n <textarea #input\n rows=\"2\"\n [attr.aria-label]=\"m().composerLabel\"\n [placeholder]=\"m().composerPlaceholder\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"suggestions().length ? listId : null\"\n [attr.aria-expanded]=\"suggestions().length > 0\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [value]=\"text()\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"></textarea>\n @if (sending()) {\n <button type=\"button\" class=\"send\" (click)=\"stop.emit()\">{{ m().stop }}</button>\n } @else {\n <button type=\"button\" class=\"send\" [disabled]=\"!canSend() || !hasContent()\" (click)=\"submit()\">{{ m().send }}</button>\n }\n </div>\n <div class=\"hint\" aria-hidden=\"true\">\n @for (command of commands; track command.command) {\n <kbd>{{ command.command }}</kbd>\n }\n </div>\n</div>\n", styles: [":host{display:block}.composer{position:relative;display:grid;gap:var(--theme-space-2);padding:10px 14px;border-top:1px solid var(--theme-border-subtle)}.box{display:flex;align-items:flex-end;gap:var(--theme-space-2);padding:6px 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-input)}.box:focus-within{box-shadow:var(--theme-focus-ring)}textarea{flex:1;min-width:0;min-height:40px;max-height:160px;border:0;outline:0;resize:vertical;background:none;color:var(--theme-text-primary);font:inherit}.send{flex:none;height:26px;padding:0 10px;border:1px solid var(--theme-accent);border-radius:var(--theme-radius-sm);background:var(--theme-accent);color:var(--theme-text-on-accent);font:inherit;font-size:.75rem;cursor:pointer}.send:disabled{border-color:var(--theme-border-default);background:none;color:var(--theme-text-muted);cursor:default}.send:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.hint{display:flex;flex-wrap:wrap;gap:var(--theme-space-2)}.hint kbd{padding:0 5px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);color:var(--theme-text-muted);font-family:var(--theme-font-mono);font-size:.6875rem}.suggestions{position:absolute;right:14px;bottom:calc(100% - 4px);left:14px;z-index:1;margin:0;padding:4px;list-style:none;border-radius:var(--theme-radius-lg);background:var(--theme-bg-overlay);box-shadow:var(--theme-shadow-popup)}.suggestions li{display:flex;gap:var(--theme-space-3);padding:6px 8px;border-radius:var(--theme-radius-sm);cursor:pointer}.suggestions li[aria-selected=true]{background:var(--theme-accent-subtle)}.cmd{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.cmd-desc{color:var(--theme-text-secondary)}.attach{flex:none;display:grid;place-items:center;width:26px;height:26px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);cursor:pointer}.attach:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.attach:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.file-input{display:none}.files{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}.file{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:22px;padding:0 4px 0 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);font-size:.75rem;color:var(--theme-text-secondary)}.file button{display:inline-grid;flex:none;place-items:center;border:0;padding:0 2px;background:none;color:var(--theme-text-muted);cursor:pointer}.file button:hover{color:var(--theme-text-primary)}.file button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.file-name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-errors{color:var(--theme-status-error);font-size:.75rem}.file-errors p{margin:0}\n"], dependencies: [{ kind: "component", type: SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
239
+ }
240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantComposerComponent, decorators: [{
241
+ type: Component,
242
+ args: [{ selector: 'mm-assistant-composer', imports: [SVGIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"composer\">\n @if (suggestions().length) {\n <ul class=\"suggestions\" role=\"listbox\" [id]=\"listId\" [attr.aria-label]=\"m().commandsLabel\">\n @for (command of suggestions(); track command.command; let i = $index) {\n <li role=\"option\" [id]=\"listId + '-' + i\" [attr.aria-selected]=\"activeOptionId() === listId + '-' + i\"\n (mousedown)=\"$event.preventDefault()\" (click)=\"pick(command)\">\n <span class=\"cmd\">{{ command.command }}</span>\n <span class=\"cmd-desc\">{{ command.description }}</span>\n </li>\n }\n </ul>\n }\n @if (attachments(); as options) {\n @if (files().length) {\n <ul class=\"files\" [attr.aria-label]=\"m().attachmentsLabel\">\n @for (file of files(); track $index; let i = $index) {\n <li class=\"file\">\n <span class=\"file-name\" [title]=\"file.name\">{{ file.name }}</span>\n <button type=\"button\" [attr.aria-label]=\"format(m().removeAttachment, { name: file.name })\" [title]=\"format(m().removeAttachment, { name: file.name })\" (click)=\"removeFile(i)\"><kendo-svg-icon [icon]=\"xIcon\" size=\"small\"></kendo-svg-icon></button>\n </li>\n }\n </ul>\n }\n @if (attachmentErrors().length) {\n <div class=\"file-errors\" role=\"alert\">\n @for (error of attachmentErrors(); track $index) {\n <p>{{ error }}</p>\n }\n </div>\n }\n <input #fileInput class=\"file-input\" type=\"file\" tabindex=\"-1\" aria-hidden=\"true\"\n [attr.accept]=\"options.accept || null\" [multiple]=\"multiple()\"\n (change)=\"onFilesPicked($any($event.target))\">\n }\n <div class=\"box\">\n @if (attachments()) {\n <button type=\"button\" class=\"attach\" [attr.aria-label]=\"m().attachFile\" [title]=\"m().attachFile\" (click)=\"openFilePicker()\">\n <kendo-svg-icon [icon]=\"paperclipIcon\"></kendo-svg-icon>\n </button>\n }\n <textarea #input\n rows=\"2\"\n [attr.aria-label]=\"m().composerLabel\"\n [placeholder]=\"m().composerPlaceholder\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"suggestions().length ? listId : null\"\n [attr.aria-expanded]=\"suggestions().length > 0\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [value]=\"text()\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"></textarea>\n @if (sending()) {\n <button type=\"button\" class=\"send\" (click)=\"stop.emit()\">{{ m().stop }}</button>\n } @else {\n <button type=\"button\" class=\"send\" [disabled]=\"!canSend() || !hasContent()\" (click)=\"submit()\">{{ m().send }}</button>\n }\n </div>\n <div class=\"hint\" aria-hidden=\"true\">\n @for (command of commands; track command.command) {\n <kbd>{{ command.command }}</kbd>\n }\n </div>\n</div>\n", styles: [":host{display:block}.composer{position:relative;display:grid;gap:var(--theme-space-2);padding:10px 14px;border-top:1px solid var(--theme-border-subtle)}.box{display:flex;align-items:flex-end;gap:var(--theme-space-2);padding:6px 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-input)}.box:focus-within{box-shadow:var(--theme-focus-ring)}textarea{flex:1;min-width:0;min-height:40px;max-height:160px;border:0;outline:0;resize:vertical;background:none;color:var(--theme-text-primary);font:inherit}.send{flex:none;height:26px;padding:0 10px;border:1px solid var(--theme-accent);border-radius:var(--theme-radius-sm);background:var(--theme-accent);color:var(--theme-text-on-accent);font:inherit;font-size:.75rem;cursor:pointer}.send:disabled{border-color:var(--theme-border-default);background:none;color:var(--theme-text-muted);cursor:default}.send:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.hint{display:flex;flex-wrap:wrap;gap:var(--theme-space-2)}.hint kbd{padding:0 5px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);color:var(--theme-text-muted);font-family:var(--theme-font-mono);font-size:.6875rem}.suggestions{position:absolute;right:14px;bottom:calc(100% - 4px);left:14px;z-index:1;margin:0;padding:4px;list-style:none;border-radius:var(--theme-radius-lg);background:var(--theme-bg-overlay);box-shadow:var(--theme-shadow-popup)}.suggestions li{display:flex;gap:var(--theme-space-3);padding:6px 8px;border-radius:var(--theme-radius-sm);cursor:pointer}.suggestions li[aria-selected=true]{background:var(--theme-accent-subtle)}.cmd{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.cmd-desc{color:var(--theme-text-secondary)}.attach{flex:none;display:grid;place-items:center;width:26px;height:26px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);cursor:pointer}.attach:hover{background:var(--theme-bg-hover);color:var(--theme-text-primary)}.attach:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.file-input{display:none}.files{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}.file{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:22px;padding:0 4px 0 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);font-size:.75rem;color:var(--theme-text-secondary)}.file button{display:inline-grid;flex:none;place-items:center;border:0;padding:0 2px;background:none;color:var(--theme-text-muted);cursor:pointer}.file button:hover{color:var(--theme-text-primary)}.file button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.file-name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-errors{color:var(--theme-status-error);font-size:.75rem}.file-errors p{margin:0}\n"] }]
243
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }, { type: i0.Output, args: ["textChange"] }], canSend: [{ type: i0.Input, args: [{ isSignal: true, alias: "canSend", required: false }] }], sending: [{ type: i0.Input, args: [{ isSignal: true, alias: "sending", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], send: [{ type: i0.Output, args: ["send"] }], stop: [{ type: i0.Output, args: ["stop"] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], textarea: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
244
+
245
+ /**
246
+ * A write the assistant proposes (ui-concept §5.3/§5.6, wireframe screen 6):
247
+ * violet card with a parameter table and Run / Edit / Discard. The buttons only
248
+ * emit — the card never executes anything; the host forwards the decision.
249
+ */
250
+ class AssistantProposalCardComponent {
251
+ proposal = input.required(/* @ts-ignore */
252
+ ...(ngDevMode ? [{ debugName: "proposal" }] : /* istanbul ignore next */ []));
253
+ /** Set once the person ran or discarded the proposal; disables the buttons. */
254
+ decision = input(null, /* @ts-ignore */
255
+ ...(ngDevMode ? [{ debugName: "decision" }] : /* istanbul ignore next */ []));
256
+ run = output();
257
+ edit = output();
258
+ discard = output();
259
+ /** Translations; members left out fall back to {@link ASSISTANT_MESSAGES}, then English. */
260
+ messages = input(null, /* @ts-ignore */
261
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
262
+ m = assistantMessages(this.messages);
263
+ cardLabel = computed(() => formatAssistantMessage(this.m().proposalLabel, { title: this.proposal().title }), /* @ts-ignore */
264
+ ...(ngDevMode ? [{ debugName: "cardLabel" }] : /* istanbul ignore next */ []));
265
+ decisionLabel = computed(() => {
266
+ const decision = this.decision();
267
+ return decision ? proposalDecisionLabel(decision, this.m()) : null;
268
+ }, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "decisionLabel" }] : /* istanbul ignore next */ []));
270
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantProposalCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
271
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: AssistantProposalCardComponent, isStandalone: true, selector: "mm-assistant-proposal-card", inputs: { proposal: { classPropertyName: "proposal", publicName: "proposal", isSignal: true, isRequired: true, transformFunction: null }, decision: { classPropertyName: "decision", publicName: "decision", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { run: "run", edit: "edit", discard: "discard" }, ngImport: i0, template: "<article class=\"proposal\" [class.settled]=\"decision() !== null\" [attr.aria-label]=\"cardLabel()\">\n <header class=\"proposal-head\">\n <span aria-hidden=\"true\">\u2726</span> {{ m().proposal }}@if (proposal().writes) { \u00B7 {{ m().writesToTenant }} }\n </header>\n <div class=\"proposal-body\">\n <p class=\"proposal-title\">{{ proposal().title }}</p>\n @if (proposal().description) {\n <p class=\"proposal-description\">{{ proposal().description }}</p>\n }\n @if (proposal().parameters.length) {\n <dl class=\"proposal-params\">\n @for (param of proposal().parameters; track param.name) {\n <dt>{{ param.name }}</dt>\n <dd>{{ param.value }}</dd>\n }\n </dl>\n }\n <p class=\"proposal-meta\">{{ m().tool }} <span class=\"mono\">{{ proposal().toolName }}</span> \u00B7 {{ m().runsAfterConfirm }}</p>\n </div>\n <footer class=\"proposal-foot\">\n @if (decisionLabel(); as label) {\n <span class=\"proposal-decision\" role=\"status\">{{ label }}</span>\n } @else {\n <button type=\"button\" class=\"btn primary\" (click)=\"run.emit(proposal())\">{{ m().run }}</button>\n <button type=\"button\" class=\"btn\" (click)=\"edit.emit(proposal())\">{{ m().edit }}</button>\n <button type=\"button\" class=\"btn\" (click)=\"discard.emit(proposal())\">{{ m().discard }}</button>\n }\n </footer>\n</article>\n", styles: [":host{display:block}.proposal{overflow:hidden;border:1px solid color-mix(in srgb,var(--theme-ai) 55%,transparent);border-radius:var(--theme-radius-md);background:var(--theme-bg-surface)}.proposal.settled{border-color:var(--theme-border-default)}.proposal-head{padding:6px 10px;background:var(--theme-ai-subtle);color:var(--theme-ai);font-size:.75rem;font-weight:500}.proposal-body{display:grid;gap:var(--theme-space-1);padding:10px;font-size:.75rem}.proposal-body p{margin:0}.proposal-title{color:var(--theme-text-primary)}.proposal-description,.proposal-meta{color:var(--theme-text-secondary)}.proposal-params{display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px var(--theme-space-3);margin:0}.proposal-params dt{color:var(--theme-text-secondary)}.proposal-params dd{margin:0;overflow-wrap:anywhere;font-family:var(--theme-font-mono)}.mono{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.proposal-foot{display:flex;align-items:center;gap:6px;padding:8px 10px;border-top:1px solid var(--theme-border-subtle)}.proposal-decision{color:var(--theme-text-secondary);font-size:.75rem}.btn{height:26px;padding:0 10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-elevated);color:var(--theme-text-primary);font:inherit;font-size:.75rem;cursor:pointer}.btn:hover{border-color:var(--theme-border-strong)}.btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.btn.primary{border-color:var(--theme-accent);background:var(--theme-accent);color:var(--theme-text-on-accent)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
272
+ }
273
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantProposalCardComponent, decorators: [{
274
+ type: Component,
275
+ args: [{ selector: 'mm-assistant-proposal-card', changeDetection: ChangeDetectionStrategy.OnPush, template: "<article class=\"proposal\" [class.settled]=\"decision() !== null\" [attr.aria-label]=\"cardLabel()\">\n <header class=\"proposal-head\">\n <span aria-hidden=\"true\">\u2726</span> {{ m().proposal }}@if (proposal().writes) { \u00B7 {{ m().writesToTenant }} }\n </header>\n <div class=\"proposal-body\">\n <p class=\"proposal-title\">{{ proposal().title }}</p>\n @if (proposal().description) {\n <p class=\"proposal-description\">{{ proposal().description }}</p>\n }\n @if (proposal().parameters.length) {\n <dl class=\"proposal-params\">\n @for (param of proposal().parameters; track param.name) {\n <dt>{{ param.name }}</dt>\n <dd>{{ param.value }}</dd>\n }\n </dl>\n }\n <p class=\"proposal-meta\">{{ m().tool }} <span class=\"mono\">{{ proposal().toolName }}</span> \u00B7 {{ m().runsAfterConfirm }}</p>\n </div>\n <footer class=\"proposal-foot\">\n @if (decisionLabel(); as label) {\n <span class=\"proposal-decision\" role=\"status\">{{ label }}</span>\n } @else {\n <button type=\"button\" class=\"btn primary\" (click)=\"run.emit(proposal())\">{{ m().run }}</button>\n <button type=\"button\" class=\"btn\" (click)=\"edit.emit(proposal())\">{{ m().edit }}</button>\n <button type=\"button\" class=\"btn\" (click)=\"discard.emit(proposal())\">{{ m().discard }}</button>\n }\n </footer>\n</article>\n", styles: [":host{display:block}.proposal{overflow:hidden;border:1px solid color-mix(in srgb,var(--theme-ai) 55%,transparent);border-radius:var(--theme-radius-md);background:var(--theme-bg-surface)}.proposal.settled{border-color:var(--theme-border-default)}.proposal-head{padding:6px 10px;background:var(--theme-ai-subtle);color:var(--theme-ai);font-size:.75rem;font-weight:500}.proposal-body{display:grid;gap:var(--theme-space-1);padding:10px;font-size:.75rem}.proposal-body p{margin:0}.proposal-title{color:var(--theme-text-primary)}.proposal-description,.proposal-meta{color:var(--theme-text-secondary)}.proposal-params{display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px var(--theme-space-3);margin:0}.proposal-params dt{color:var(--theme-text-secondary)}.proposal-params dd{margin:0;overflow-wrap:anywhere;font-family:var(--theme-font-mono)}.mono{font-family:var(--theme-font-mono);color:var(--theme-text-primary)}.proposal-foot{display:flex;align-items:center;gap:6px;padding:8px 10px;border-top:1px solid var(--theme-border-subtle)}.proposal-decision{color:var(--theme-text-secondary);font-size:.75rem}.btn{height:26px;padding:0 10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:var(--theme-bg-elevated);color:var(--theme-text-primary);font:inherit;font-size:.75rem;cursor:pointer}.btn:hover{border-color:var(--theme-border-strong)}.btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.btn.primary{border-color:var(--theme-accent);background:var(--theme-accent);color:var(--theme-text-on-accent)}\n"] }]
276
+ }], propDecorators: { proposal: [{ type: i0.Input, args: [{ isSignal: true, alias: "proposal", required: true }] }], decision: [{ type: i0.Input, args: [{ isSignal: true, alias: "decision", required: false }] }], run: [{ type: i0.Output, args: ["run"] }], edit: [{ type: i0.Output, args: ["edit"] }], discard: [{ type: i0.Output, args: ["discard"] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }] } });
277
+
278
+ /**
279
+ * The conversation (wireframe screen 6): user turns, assistant turns (markdown
280
+ * via `renderAssistantMarkdown`: no images, safe links only; violet edge), tool-call rows and proposal cards. Tool calls
281
+ * render with the `ASSISTANT_TOOL_CALL_COMPONENT` (an OctoMesh host passes `mm-ai-tool-call` from
282
+ * `@meshmakers/octo-ai-console`, ui-concept §5.1 "do not rebuild"); proposals are emitted, never executed.
283
+ *
284
+ * A user turn sent through a slash command's `expand` (AB#5621) shows the expanded prompt with the
285
+ * typed command (`slashCommand`) as a small label above it.
286
+ *
287
+ * While an assistant turn streams it shows the OctoBot `thinking` with "Thinking…" (AB#3444);
288
+ * once the reply is complete the newest assistant turn keeps the OctoBot as a still frame. The
289
+ * figure is decorative — the text carries the meaning — and appears at most once in the thread.
290
+ *
291
+ * The list is an `aria-live` log; it is `aria-busy` while a turn streams so a
292
+ * screen reader announces finished messages instead of every token. Nothing
293
+ * inside it carries its own live role, so each addition is announced once.
294
+ */
295
+ class AssistantThreadComponent {
296
+ host = inject(ElementRef);
297
+ items = input([], /* @ts-ignore */
298
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
299
+ proposalDecision = output();
300
+ /** Translations; members left out fall back to {@link ASSISTANT_MESSAGES}, then English. */
301
+ messages = input(null, /* @ts-ignore */
302
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
303
+ m = assistantMessages(this.messages);
304
+ toolCallComponent = inject(ASSISTANT_TOOL_CALL_COMPONENT);
305
+ /** Id of the newest assistant turn: the only one that shows the resting OctoBot. */
306
+ lastAssistantId = computed(() => {
307
+ const items = this.items();
308
+ for (let i = items.length - 1; i >= 0; i--) {
309
+ if (items[i].kind === 'assistant') {
310
+ return items[i].id;
311
+ }
312
+ }
313
+ return null;
314
+ }, /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "lastAssistantId" }] : /* istanbul ignore next */ []));
316
+ streaming = computed(() => this.items().some(item => item.kind === 'assistant' && item.streaming), /* @ts-ignore */
317
+ ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
318
+ constructor() {
319
+ // Keep the newest turn in view.
320
+ effect(() => {
321
+ this.items();
322
+ queueMicrotask(() => {
323
+ const element = this.host.nativeElement;
324
+ element.scrollTop = element.scrollHeight;
325
+ });
326
+ });
327
+ }
328
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantThreadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
329
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: AssistantThreadComponent, isStandalone: true, selector: "mm-assistant-thread", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { proposalDecision: "proposalDecision" }, ngImport: i0, template: "<ol class=\"thread\" role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" [attr.aria-label]=\"m().conversationLabel\"\n [attr.aria-busy]=\"streaming()\">\n @for (item of items(); track item.id) {\n @switch (item.kind) {\n @case ('user') {\n <li class=\"msg user\"><span class=\"sr-only\">{{ m().youPrefix }}</span>@if (item.slashCommand) {<span class=\"slash-command\"><span class=\"sr-only\">{{ m().slashCommandPrefix }}</span>{{ item.slashCommand }}</span>}{{ item.text }}@if (item.attachments?.length) {<ul class=\"attachments\" [attr.aria-label]=\"m().attachmentsLabel\">@for (file of item.attachments; track $index) {<li class=\"attachment\">{{ file.name }}</li>}</ul>}</li>\n }\n @case ('assistant') {\n <li class=\"msg bot\">\n <span class=\"sr-only\">{{ m().assistantPrefix }}</span>\n <div class=\"md\" [innerHTML]=\"item.markdown | assistantMarkdown\"></div>\n @if (item.streaming) {\n <span class=\"streaming\" data-testid=\"assistant-thinking\">\n <mm-octobot animation=\"thinking\" size=\"sm\" />\n <span class=\"streaming-text\">{{ m().thinking }}</span>\n </span>\n } @else if (item.id === lastAssistantId()) {\n <mm-octobot class=\"settled\" animation=\"thinking\" size=\"sm\" still data-testid=\"assistant-settled\" />\n }\n </li>\n }\n @case ('tool-call') {\n <li class=\"msg tool\"><ng-container *ngComponentOutlet=\"toolCallComponent; inputs: { call: item.call }\"></ng-container></li>\n }\n @case ('proposal') {\n <li class=\"msg\">\n <mm-assistant-proposal-card\n [messages]=\"messages()\"\n [proposal]=\"item.proposal\"\n [decision]=\"item.decision\"\n (run)=\"proposalDecision.emit({ kind: 'run', proposal: $event })\"\n (edit)=\"proposalDecision.emit({ kind: 'edit', proposal: $event })\"\n (discard)=\"proposalDecision.emit({ kind: 'discard', proposal: $event })\">\n </mm-assistant-proposal-card>\n </li>\n }\n @case ('error') {\n <li class=\"msg error\"><span class=\"sr-only\">{{ m().errorPrefix }}</span>{{ item.text }}</li>\n }\n }\n }\n</ol>\n", styles: [":host{display:block;overflow:auto}.thread{display:grid;align-content:start;gap:var(--theme-space-3);margin:0;padding:14px;list-style:none}.msg{display:grid;gap:6px;min-width:0;overflow-wrap:anywhere}.user{justify-self:end;max-width:85%;padding:8px 12px;border-radius:var(--theme-radius-md);background:var(--theme-bg-overlay);white-space:pre-wrap}.slash-command{color:var(--theme-text-muted);font-family:var(--theme-font-mono, monospace);font-size:.71875rem;overflow-wrap:anywhere}.attachments{display:flex;flex-wrap:wrap;gap:4px;margin:4px 0 0;padding:0;list-style:none;white-space:normal}.attachment{padding:0 6px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);color:var(--theme-text-secondary);font-size:.75rem}.bot{padding-left:12px;border-left:2px solid var(--theme-ai)}.bot ::ng-deep p{margin:0 0 var(--theme-space-2)}.bot ::ng-deep a{color:var(--theme-text-accent)}.streaming{display:inline-flex;align-items:center;gap:var(--theme-space-2);color:var(--theme-text-secondary);font-size:.8125rem}.settled{opacity:.85}.error{color:var(--theme-status-error)}mm-ai-tool-call{--mm-ai-tool-bg: var(--theme-bg-app);--mm-ai-tool-border: var(--theme-border-default);--mm-ai-tool-text: var(--theme-text-primary);--mm-ai-tool-muted: var(--theme-text-secondary);--mm-ai-tool-success: var(--theme-status-success);--mm-ai-tool-failed: var(--theme-status-error);--mm-ai-tool-rejected: var(--theme-status-warning);--mm-ai-tool-pending: var(--theme-status-neutral);--mm-ai-tool-code-bg: var(--theme-bg-sunken)}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AssistantProposalCardComponent, selector: "mm-assistant-proposal-card", inputs: ["proposal", "decision", "messages"], outputs: ["run", "edit", "discard"] }, { kind: "component", type: OctoBotComponent, selector: "mm-octobot", inputs: ["animation", "size", "label", "loading", "still", "playOnce"] }, { kind: "pipe", type: AssistantMarkdownPipe, name: "assistantMarkdown" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
330
+ }
331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantThreadComponent, decorators: [{
332
+ type: Component,
333
+ args: [{ selector: 'mm-assistant-thread', imports: [NgComponentOutlet, AssistantMarkdownPipe, AssistantProposalCardComponent, OctoBotComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ol class=\"thread\" role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" [attr.aria-label]=\"m().conversationLabel\"\n [attr.aria-busy]=\"streaming()\">\n @for (item of items(); track item.id) {\n @switch (item.kind) {\n @case ('user') {\n <li class=\"msg user\"><span class=\"sr-only\">{{ m().youPrefix }}</span>@if (item.slashCommand) {<span class=\"slash-command\"><span class=\"sr-only\">{{ m().slashCommandPrefix }}</span>{{ item.slashCommand }}</span>}{{ item.text }}@if (item.attachments?.length) {<ul class=\"attachments\" [attr.aria-label]=\"m().attachmentsLabel\">@for (file of item.attachments; track $index) {<li class=\"attachment\">{{ file.name }}</li>}</ul>}</li>\n }\n @case ('assistant') {\n <li class=\"msg bot\">\n <span class=\"sr-only\">{{ m().assistantPrefix }}</span>\n <div class=\"md\" [innerHTML]=\"item.markdown | assistantMarkdown\"></div>\n @if (item.streaming) {\n <span class=\"streaming\" data-testid=\"assistant-thinking\">\n <mm-octobot animation=\"thinking\" size=\"sm\" />\n <span class=\"streaming-text\">{{ m().thinking }}</span>\n </span>\n } @else if (item.id === lastAssistantId()) {\n <mm-octobot class=\"settled\" animation=\"thinking\" size=\"sm\" still data-testid=\"assistant-settled\" />\n }\n </li>\n }\n @case ('tool-call') {\n <li class=\"msg tool\"><ng-container *ngComponentOutlet=\"toolCallComponent; inputs: { call: item.call }\"></ng-container></li>\n }\n @case ('proposal') {\n <li class=\"msg\">\n <mm-assistant-proposal-card\n [messages]=\"messages()\"\n [proposal]=\"item.proposal\"\n [decision]=\"item.decision\"\n (run)=\"proposalDecision.emit({ kind: 'run', proposal: $event })\"\n (edit)=\"proposalDecision.emit({ kind: 'edit', proposal: $event })\"\n (discard)=\"proposalDecision.emit({ kind: 'discard', proposal: $event })\">\n </mm-assistant-proposal-card>\n </li>\n }\n @case ('error') {\n <li class=\"msg error\"><span class=\"sr-only\">{{ m().errorPrefix }}</span>{{ item.text }}</li>\n }\n }\n }\n</ol>\n", styles: [":host{display:block;overflow:auto}.thread{display:grid;align-content:start;gap:var(--theme-space-3);margin:0;padding:14px;list-style:none}.msg{display:grid;gap:6px;min-width:0;overflow-wrap:anywhere}.user{justify-self:end;max-width:85%;padding:8px 12px;border-radius:var(--theme-radius-md);background:var(--theme-bg-overlay);white-space:pre-wrap}.slash-command{color:var(--theme-text-muted);font-family:var(--theme-font-mono, monospace);font-size:.71875rem;overflow-wrap:anywhere}.attachments{display:flex;flex-wrap:wrap;gap:4px;margin:4px 0 0;padding:0;list-style:none;white-space:normal}.attachment{padding:0 6px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);color:var(--theme-text-secondary);font-size:.75rem}.bot{padding-left:12px;border-left:2px solid var(--theme-ai)}.bot ::ng-deep p{margin:0 0 var(--theme-space-2)}.bot ::ng-deep a{color:var(--theme-text-accent)}.streaming{display:inline-flex;align-items:center;gap:var(--theme-space-2);color:var(--theme-text-secondary);font-size:.8125rem}.settled{opacity:.85}.error{color:var(--theme-status-error)}mm-ai-tool-call{--mm-ai-tool-bg: var(--theme-bg-app);--mm-ai-tool-border: var(--theme-border-default);--mm-ai-tool-text: var(--theme-text-primary);--mm-ai-tool-muted: var(--theme-text-secondary);--mm-ai-tool-success: var(--theme-status-success);--mm-ai-tool-failed: var(--theme-status-error);--mm-ai-tool-rejected: var(--theme-status-warning);--mm-ai-tool-pending: var(--theme-status-neutral);--mm-ai-tool-code-bg: var(--theme-bg-sunken)}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}\n"] }]
334
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], proposalDecision: [{ type: i0.Output, args: ["proposalDecision"] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }] } });
335
+
336
+ /** Width of the panel before anyone resized it (CSS: 400 px, 340 px ≤ 1180 px), used until measured. */
337
+ const FALLBACK_WIDTH = 400;
338
+ /** Window width the handle always leaves to the content. */
339
+ const MIN_CONTENT_WIDTH = 240;
340
+ /** Keyboard step of the resize handle in px (Shift: four times). */
341
+ const RESIZE_STEP = 16;
342
+ /** The persisted width, or null when absent, invalid or storage is unavailable. */
343
+ function readStoredWidth(key) {
344
+ if (!key) {
345
+ return null;
346
+ }
347
+ try {
348
+ const raw = globalThis.localStorage?.getItem(key);
349
+ const width = raw === null || raw === undefined ? NaN : Number(raw);
350
+ return Number.isFinite(width) && width > 0 ? width : null;
351
+ }
352
+ catch {
353
+ return null;
354
+ }
355
+ }
356
+ /** Best effort: storage may be missing, full or blocked. */
357
+ function writeStoredWidth(key, width) {
358
+ if (!key) {
359
+ return;
360
+ }
361
+ try {
362
+ if (width === null) {
363
+ globalThis.localStorage?.removeItem(key);
364
+ }
365
+ else {
366
+ globalThis.localStorage?.setItem(key, String(Math.round(width)));
367
+ }
368
+ }
369
+ catch {
370
+ // Persistence is best effort.
371
+ }
372
+ }
373
+ /**
374
+ * Right-side assistant panel (ui-concept §5.3, wireframe screen 6): header,
375
+ * removable context chips, thread, composer. Non-modal: the page stays usable.
376
+ * Opening focuses the composer, Esc closes and focus returns to where it was
377
+ * (AssistantService). Below 640 px it covers the screen under the top bar and
378
+ * becomes a modal dialog (`aria-modal`, Tab trapped) so the page behind is out
379
+ * of reach. Removing a context chip moves focus to the next chip, else the composer.
380
+ *
381
+ * Host it only while {@link AssistantService.enabled} is set and the panel is open
382
+ * (`@if (assistant.enabled && assistant.isOpen()) { <mm-assistant-panel /> }`); the thread
383
+ * lives in the service, so closing and navigating keep it.
384
+ *
385
+ * Optional transport capabilities (AB#5621) add UI only when the transport has them:
386
+ * - **sessions** — "New chat" and "Chats" in the header; the chat list replaces the thread
387
+ * (open a chat, delete with an inline confirmation; Esc returns to the chat);
388
+ * - **starter questions** — shown as buttons on an empty thread; clicking one sends it;
389
+ * - **attachments** — the composer's "Attach file" button (see `mm-assistant-composer`).
390
+ *
391
+ * Header actions ("New chat", "Chats", close) are icon buttons with tooltip and `aria-label` from
392
+ * the messages, so they never wrap in a narrow panel (AB#5621).
393
+ *
394
+ * **Layout (AB#5621)** — inputs, else {@link ASSISTANT_PANEL_OPTIONS}, else the defaults:
395
+ * - `resizable` (default `false`, so no handle and no extra tab stop; opt in per input or token):
396
+ * a handle on the left edge (`role="separator"`) changes the width by dragging or with ←/→
397
+ * (Shift: bigger steps), Home/End (min/max); a double click forgets the chosen width. The width
398
+ * stays within `minWidth`..`maxWidth` (320..720 px, and at least 240 px of the window stay free)
399
+ * and is kept in `localStorage` under `storageKey` (`mm-assistant-panel-width`; `null` = not
400
+ * kept). Without `resizable`, or until someone resizes it, the panel keeps its CSS width (400 px,
401
+ * 340 px ≤ 1180 px). Unavailable storage only means the width is not kept.
402
+ * - `mode`: `docked` (default, pushes the content) or `overlay` (floats over it, `position: fixed`
403
+ * at the right edge below `--mm-assistant-overlay-top`, default 48 px; stacking via
404
+ * `--mm-assistant-overlay-z-index`, default 20).
405
+ * On phones neither applies: the panel is the full-screen dialog.
406
+ */
407
+ class AssistantPanelComponent {
408
+ assistant = inject(AssistantService);
409
+ injector = inject(Injector);
410
+ host = inject(ElementRef);
411
+ composer = viewChild(AssistantComposerComponent, /* @ts-ignore */
412
+ ...(ngDevMode ? [{ debugName: "composer" }] : /* istanbul ignore next */ []));
413
+ /** Translations; members left out fall back to {@link ASSISTANT_MESSAGES}, then English. */
414
+ messages = input(null, /* @ts-ignore */
415
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
416
+ m = assistantMessages(this.messages);
417
+ /** `docked` pushes the content, `overlay` floats over it (default: options token, else `docked`). */
418
+ mode = input(null, /* @ts-ignore */
419
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
420
+ /** Shows the resize handle (default: options token, else `false`). */
421
+ resizable = input(null, /* @ts-ignore */
422
+ ...(ngDevMode ? [{ debugName: "resizable" }] : /* istanbul ignore next */ []));
423
+ /** Smallest width in px of the resize handle (default: options token, else 320). */
424
+ minWidth = input(null, /* @ts-ignore */
425
+ ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
426
+ /** Largest width in px of the resize handle (default: options token, else 720). */
427
+ maxWidth = input(null, /* @ts-ignore */
428
+ ...(ngDevMode ? [{ debugName: "maxWidth" }] : /* istanbul ignore next */ []));
429
+ /**
430
+ * `localStorage` key of the chosen width; `null` = not kept. Left out (`undefined`): options
431
+ * token, else `mm-assistant-panel-width`.
432
+ */
433
+ storageKey = input(undefined, /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
435
+ xIcon = xIcon;
436
+ plusIcon = plusIcon;
437
+ historyIcon = clockArrowRotateIcon;
438
+ trashIcon = trashIcon;
439
+ options = inject(ASSISTANT_PANEL_OPTIONS, { optional: true });
440
+ effectiveMode = computed(() => this.mode() ?? this.options?.mode ?? 'docked', /* @ts-ignore */
441
+ ...(ngDevMode ? [{ debugName: "effectiveMode" }] : /* istanbul ignore next */ []));
442
+ overlay = computed(() => this.effectiveMode() === 'overlay' && !this.isPhone(), /* @ts-ignore */
443
+ ...(ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []));
444
+ effectiveStorageKey = computed(() => {
445
+ const key = this.storageKey();
446
+ if (key !== undefined) {
447
+ return key;
448
+ }
449
+ return this.options?.storageKey !== undefined ? this.options.storageKey : DEFAULT_ASSISTANT_PANEL_STORAGE_KEY;
450
+ }, /* @ts-ignore */
451
+ ...(ngDevMode ? [{ debugName: "effectiveStorageKey" }] : /* istanbul ignore next */ []));
452
+ effectiveMin = computed(() => Math.max(0, Math.round(this.minWidth() ?? this.options?.minWidth ?? DEFAULT_ASSISTANT_PANEL_MIN_WIDTH)), /* @ts-ignore */
453
+ ...(ngDevMode ? [{ debugName: "effectiveMin" }] : /* istanbul ignore next */ []));
454
+ effectiveMax = computed(() => {
455
+ const configured = this.maxWidth() ?? this.options?.maxWidth ?? DEFAULT_ASSISTANT_PANEL_MAX_WIDTH;
456
+ const viewport = typeof window !== 'undefined' && window.innerWidth > 0 ? window.innerWidth - MIN_CONTENT_WIDTH : Infinity;
457
+ return Math.round(Math.max(this.effectiveMin(), Math.min(configured, viewport)));
458
+ }, /* @ts-ignore */
459
+ ...(ngDevMode ? [{ debugName: "effectiveMax" }] : /* istanbul ignore next */ []));
460
+ /** The handle is shown (never on phones). */
461
+ canResize = computed(() => (this.resizable() ?? this.options?.resizable ?? false) && !this.isPhone(), /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "canResize" }] : /* istanbul ignore next */ []));
463
+ /** Width chosen in this panel instance; `undefined` = not touched yet (stored width applies). */
464
+ chosenWidth = signal(undefined, /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "chosenWidth" }] : /* istanbul ignore next */ []));
466
+ storedWidth = computed(() => readStoredWidth(this.effectiveStorageKey()), /* @ts-ignore */
467
+ ...(ngDevMode ? [{ debugName: "storedWidth" }] : /* istanbul ignore next */ []));
468
+ /** The inline width in px, or null for the CSS default. */
469
+ appliedWidth = computed(() => {
470
+ const chosen = this.chosenWidth();
471
+ const width = chosen === undefined ? this.storedWidth() : chosen;
472
+ return width === null || !this.canResize() ? null : this.clampWidth(width);
473
+ }, /* @ts-ignore */
474
+ ...(ngDevMode ? [{ debugName: "appliedWidth" }] : /* istanbul ignore next */ []));
475
+ /** CSS width as rendered, for `aria-valuenow` before the first resize. */
476
+ measuredWidth = signal(FALLBACK_WIDTH, /* @ts-ignore */
477
+ ...(ngDevMode ? [{ debugName: "measuredWidth" }] : /* istanbul ignore next */ []));
478
+ currentWidth = computed(() => this.appliedWidth() ?? this.clampWidth(this.measuredWidth()), /* @ts-ignore */
479
+ ...(ngDevMode ? [{ debugName: "currentWidth" }] : /* istanbul ignore next */ []));
480
+ resizing = signal(false, /* @ts-ignore */
481
+ ...(ngDevMode ? [{ debugName: "resizing" }] : /* istanbul ignore next */ []));
482
+ stopDrag = null;
483
+ /** Phone layout (≤ 640 px, the shell's breakpoint): the panel is a modal dialog. */
484
+ isPhone = signal(false, /* @ts-ignore */
485
+ ...(ngDevMode ? [{ debugName: "isPhone" }] : /* istanbul ignore next */ []));
486
+ connected = computed(() => this.assistant.transportStatus() !== 'unavailable', /* @ts-ignore */
487
+ ...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
488
+ /** `chat` = thread + composer; `sessions` = the saved chats list (session-capable transports only). */
489
+ view = signal('chat', /* @ts-ignore */
490
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
491
+ /** Session whose deletion waits for confirmation. */
492
+ confirmingDelete = signal(null, /* @ts-ignore */
493
+ ...(ngDevMode ? [{ debugName: "confirmingDelete" }] : /* istanbul ignore next */ []));
494
+ /** Session being opened or deleted (its buttons are disabled meanwhile). */
495
+ busySession = signal(null, /* @ts-ignore */
496
+ ...(ngDevMode ? [{ debugName: "busySession" }] : /* istanbul ignore next */ []));
497
+ /**
498
+ * Greeting of an empty chat (AB#3444): the OctoBot waves once, then rests on its still frame.
499
+ * Not shown while the assistant is unavailable (the panel says so instead).
500
+ */
501
+ showGreeting = computed(() => this.view() === 'chat' && this.assistant.thread().length === 0 && this.connected()
502
+ && this.assistant.transportStatus() !== 'error', /* @ts-ignore */
503
+ ...(ngDevMode ? [{ debugName: "showGreeting" }] : /* istanbul ignore next */ []));
504
+ /** Starter questions on an empty thread (transports with `starterQuestions` only). */
505
+ showStarters = computed(() => this.view() === 'chat' && this.assistant.thread().length === 0 && this.assistant.starterQuestions().length > 0, /* @ts-ignore */
506
+ ...(ngDevMode ? [{ debugName: "showStarters" }] : /* istanbul ignore next */ []));
507
+ constructor() {
508
+ const query = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
509
+ ? window.matchMedia('(max-width: 640px)')
510
+ : null;
511
+ if (query) {
512
+ this.isPhone.set(query.matches);
513
+ const listener = (event) => this.isPhone.set(event.matches);
514
+ query.addEventListener('change', listener);
515
+ inject(DestroyRef).onDestroy(() => query.removeEventListener('change', listener));
516
+ }
517
+ this.assistant.loadStarterQuestions();
518
+ afterNextRender(() => this.measure());
519
+ inject(DestroyRef).onDestroy(() => this.stopDrag?.());
520
+ // Every entry point bumps focusRequest; the composer gets focus once rendered.
521
+ effect(() => {
522
+ this.assistant.focusRequest();
523
+ untracked(() => {
524
+ this.view.set('chat');
525
+ this.focusComposer();
526
+ });
527
+ });
528
+ }
529
+ /** Pointer drag on the handle: the panel sits on the right, so moving left widens it. */
530
+ startResize(event) {
531
+ if (event.button !== 0 || !this.canResize()) {
532
+ return;
533
+ }
534
+ event.preventDefault();
535
+ this.stopDrag?.();
536
+ const startX = event.clientX;
537
+ const startWidth = this.currentWidth();
538
+ const move = (moveEvent) => this.setWidth(startWidth + startX - moveEvent.clientX, false);
539
+ const end = () => {
540
+ this.stopDrag?.();
541
+ const width = this.chosenWidth();
542
+ if (width !== undefined) {
543
+ writeStoredWidth(this.effectiveStorageKey(), width);
544
+ }
545
+ };
546
+ document.addEventListener('pointermove', move);
547
+ document.addEventListener('pointerup', end);
548
+ document.addEventListener('pointercancel', end);
549
+ this.resizing.set(true);
550
+ this.stopDrag = () => {
551
+ document.removeEventListener('pointermove', move);
552
+ document.removeEventListener('pointerup', end);
553
+ document.removeEventListener('pointercancel', end);
554
+ this.resizing.set(false);
555
+ this.stopDrag = null;
556
+ };
557
+ }
558
+ /** ←/→ resize (Shift: ×4), Home/End jump to min/max; the width is kept right away. */
559
+ onResizeKey(event) {
560
+ const step = event.shiftKey ? RESIZE_STEP * 4 : RESIZE_STEP;
561
+ let width;
562
+ switch (event.key) {
563
+ case 'ArrowLeft':
564
+ width = this.currentWidth() + step;
565
+ break;
566
+ case 'ArrowRight':
567
+ width = this.currentWidth() - step;
568
+ break;
569
+ case 'Home':
570
+ width = this.effectiveMin();
571
+ break;
572
+ case 'End':
573
+ width = this.effectiveMax();
574
+ break;
575
+ default: return;
576
+ }
577
+ event.preventDefault();
578
+ this.setWidth(width, true);
579
+ }
580
+ /** Double click on the handle: back to the default width, forgetting the kept one. */
581
+ resetWidth() {
582
+ this.chosenWidth.set(null);
583
+ writeStoredWidth(this.effectiveStorageKey(), null);
584
+ afterNextRender(() => this.measure(), { injector: this.injector });
585
+ }
586
+ setWidth(width, persist) {
587
+ const clamped = this.clampWidth(width);
588
+ this.chosenWidth.set(clamped);
589
+ if (persist) {
590
+ writeStoredWidth(this.effectiveStorageKey(), clamped);
591
+ }
592
+ }
593
+ clampWidth(width) {
594
+ return Math.round(Math.min(this.effectiveMax(), Math.max(this.effectiveMin(), width)));
595
+ }
596
+ measure() {
597
+ const width = this.host.nativeElement.getBoundingClientRect().width;
598
+ if (width > 0) {
599
+ this.measuredWidth.set(width);
600
+ }
601
+ }
602
+ onEscape(event) {
603
+ if (event.defaultPrevented) {
604
+ return;
605
+ }
606
+ event.preventDefault();
607
+ if (this.confirmingDelete()) {
608
+ this.cancelDelete();
609
+ }
610
+ else if (this.view() === 'sessions') {
611
+ this.backToChat();
612
+ }
613
+ else {
614
+ this.assistant.close();
615
+ }
616
+ }
617
+ /** Shows the saved chats and reloads them. */
618
+ showSessions() {
619
+ this.confirmingDelete.set(null);
620
+ this.view.set('sessions');
621
+ void this.assistant.refreshSessions();
622
+ this.focusSessionList();
623
+ }
624
+ backToChat() {
625
+ this.confirmingDelete.set(null);
626
+ this.view.set('chat');
627
+ this.focusComposer();
628
+ }
629
+ newChat() {
630
+ this.assistant.newThread();
631
+ this.backToChat();
632
+ }
633
+ async openSession(session) {
634
+ this.busySession.set(session.id);
635
+ try {
636
+ await this.assistant.openSession(session.id);
637
+ }
638
+ finally {
639
+ this.busySession.set(null);
640
+ }
641
+ this.backToChat();
642
+ }
643
+ askDelete(session) {
644
+ this.confirmingDelete.set(session.id);
645
+ afterNextRender(() => this.host.nativeElement.querySelector('.session-confirm .confirm-delete')?.focus(), { injector: this.injector });
646
+ }
647
+ cancelDelete() {
648
+ const id = this.confirmingDelete();
649
+ this.confirmingDelete.set(null);
650
+ afterNextRender(() => Array.from(this.host.nativeElement.querySelectorAll('.session-row'))
651
+ .find(row => row.dataset['sessionId'] === id)?.querySelector('.session-delete')?.focus(), { injector: this.injector });
652
+ }
653
+ async confirmDelete(session) {
654
+ this.busySession.set(session.id);
655
+ let deleted;
656
+ try {
657
+ deleted = await this.assistant.deleteSession(session.id);
658
+ }
659
+ finally {
660
+ this.busySession.set(null);
661
+ this.confirmingDelete.set(null);
662
+ }
663
+ if (deleted) {
664
+ await this.assistant.refreshSessions();
665
+ this.focusSessionList();
666
+ }
667
+ else {
668
+ // The error row lives in the thread.
669
+ this.backToChat();
670
+ }
671
+ }
672
+ sendStarter(question) {
673
+ this.assistant.send(question);
674
+ }
675
+ /** Locale date + time of a session, or '' when absent or unparsable. */
676
+ sessionDate(session) {
677
+ if (!session.createdAt) {
678
+ return '';
679
+ }
680
+ const date = new Date(session.createdAt);
681
+ return Number.isNaN(date.getTime()) ? '' : date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
682
+ }
683
+ /** First chat of the list, else the back button. */
684
+ focusSessionList() {
685
+ afterNextRender(() => {
686
+ const root = this.host.nativeElement;
687
+ (root.querySelector('.session-open') ?? root.querySelector('.back'))?.focus();
688
+ }, { injector: this.injector });
689
+ }
690
+ focusComposer() {
691
+ afterNextRender(() => this.composer()?.focus(), { injector: this.injector });
692
+ }
693
+ /** Phone dialog: Tab and Shift+Tab cycle inside the panel. */
694
+ onTab(event) {
695
+ if (!this.isPhone() || event.defaultPrevented) {
696
+ return;
697
+ }
698
+ const focusable = Array.from(this.host.nativeElement.querySelectorAll('button:not([disabled]), textarea:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'));
699
+ if (!focusable.length) {
700
+ return;
701
+ }
702
+ const first = focusable[0];
703
+ const last = focusable[focusable.length - 1];
704
+ const active = document.activeElement;
705
+ const backwards = event.shiftKey;
706
+ if (backwards && (active === first || !this.host.nativeElement.contains(active))) {
707
+ event.preventDefault();
708
+ last.focus();
709
+ }
710
+ else if (!backwards && (active === last || !this.host.nativeElement.contains(active))) {
711
+ event.preventDefault();
712
+ first.focus();
713
+ }
714
+ }
715
+ /** Drops a chip and keeps keyboard users in the row: next chip, else the composer. */
716
+ removeChip(id, index) {
717
+ this.assistant.removeChip(id);
718
+ afterNextRender(() => {
719
+ const buttons = this.host.nativeElement.querySelectorAll('.ctx-chip button');
720
+ const next = buttons[Math.min(index, buttons.length - 1)];
721
+ if (next) {
722
+ next.focus();
723
+ }
724
+ else {
725
+ this.composer()?.focus();
726
+ }
727
+ }, { injector: this.injector });
728
+ }
729
+ format(message, values) {
730
+ return formatAssistantMessage(message, values);
731
+ }
732
+ onDecision(decision) {
733
+ this.assistant.decideProposal(decision.kind, decision.proposal);
734
+ }
735
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
736
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: AssistantPanelComponent, isStandalone: true, selector: "mm-assistant-panel", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-labelledby": "assistant-panel-title", "id": "assistant-panel" }, listeners: { "keydown.escape": "onEscape($event)", "keydown.tab": "onTab($event)", "keydown.shift.tab": "onTab($event)" }, properties: { "attr.role": "isPhone() ? \"dialog\" : \"complementary\"", "attr.aria-modal": "isPhone() ? \"true\" : null", "class.mm-assistant-overlay": "overlay()", "class.mm-assistant-resizing": "resizing()", "attr.data-mode": "effectiveMode()", "style.width.px": "isPhone() ? null : appliedWidth()" } }, viewQueries: [{ propertyName: "composer", first: true, predicate: AssistantComposerComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (canResize()) {\n <div class=\"resize-handle\" role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\" aria-controls=\"assistant-panel\"\n [attr.aria-label]=\"m().resizePanel\" [title]=\"m().resizePanel\"\n [attr.aria-valuenow]=\"currentWidth()\" [attr.aria-valuemin]=\"effectiveMin()\" [attr.aria-valuemax]=\"effectiveMax()\"\n (pointerdown)=\"startResize($event)\" (keydown)=\"onResizeKey($event)\" (dblclick)=\"resetWidth()\"></div>\n}\n\n<header class=\"head\">\n <span class=\"sparkle\" aria-hidden=\"true\">\u2726</span>\n <h2 class=\"title\" id=\"assistant-panel-title\">{{ m().title }}</h2>\n @if (connected()) {\n <span class=\"chip-neutral\" [title]=\"m().readsWithYourPermissions\">{{ m().readsWithYourPermissions }}</span>\n }\n @if (assistant.sessionsSupported) {\n <div class=\"head-actions\">\n <button type=\"button\" class=\"icon-btn new-chat\" [attr.aria-label]=\"m().newChat\" [title]=\"m().newChat\" (click)=\"newChat()\">\n <kendo-svg-icon [icon]=\"plusIcon\"></kendo-svg-icon>\n </button>\n <button type=\"button\" class=\"icon-btn sessions-toggle\" aria-controls=\"assistant-sessions\"\n [attr.aria-label]=\"m().sessions\" [title]=\"m().sessions\"\n [attr.aria-expanded]=\"view() === 'sessions'\"\n (click)=\"view() === 'sessions' ? backToChat() : showSessions()\">\n <kendo-svg-icon [icon]=\"historyIcon\"></kendo-svg-icon>\n </button>\n </div>\n }\n <button type=\"button\" class=\"icon-btn close\" [attr.aria-label]=\"m().closeAssistant\" [title]=\"m().closeAssistant\" (click)=\"assistant.close()\">\n <kendo-svg-icon [icon]=\"xIcon\"></kendo-svg-icon>\n </button>\n</header>\n\n@if (assistant.contextChips().length) {\n <ul class=\"ctx\" [attr.aria-label]=\"m().contextLabel\">\n @for (chip of assistant.contextChips(); track chip.id; let i = $index) {\n <li class=\"ctx-chip\" [attr.data-kind]=\"chip.kind\" [title]=\"chip.value\">\n <span class=\"ctx-label\">{{ chip.label }}</span>\n <button type=\"button\" [attr.aria-label]=\"format(m().removeContext, { label: chip.label })\" [title]=\"format(m().removeContext, { label: chip.label })\" (click)=\"removeChip(chip.id, i)\"><kendo-svg-icon [icon]=\"xIcon\" size=\"small\"></kendo-svg-icon></button>\n </li>\n }\n </ul>\n}\n\n@if (view() === 'sessions') {\n <section class=\"sessions\" id=\"assistant-sessions\" [attr.aria-label]=\"m().sessionsLabel\">\n <button type=\"button\" class=\"text-btn back\" (click)=\"backToChat()\">\u2190 {{ m().backToChat }}</button>\n <ul class=\"session-list\">\n @for (session of assistant.sessions(); track session.id) {\n <li class=\"session-row\" [attr.data-session-id]=\"session.id\"\n [class.active]=\"session.id === assistant.activeSessionId()\">\n @if (confirmingDelete() === session.id) {\n <div class=\"session-confirm\" role=\"group\" [attr.aria-label]=\"format(m().deleteSessionConfirm, { title: session.title })\">\n <span class=\"confirm-text\">{{ format(m().deleteSessionConfirm, { title: session.title }) }}</span>\n <button type=\"button\" class=\"text-btn danger confirm-delete\" [disabled]=\"busySession() === session.id\"\n (click)=\"confirmDelete(session)\">{{ m().delete }}</button>\n <button type=\"button\" class=\"text-btn cancel-delete\" (click)=\"cancelDelete()\">{{ m().cancel }}</button>\n </div>\n } @else {\n <button type=\"button\" class=\"session-open\" [title]=\"session.title\"\n [attr.aria-current]=\"session.id === assistant.activeSessionId() ? 'true' : null\"\n [disabled]=\"busySession() === session.id\"\n (click)=\"openSession(session)\">\n <span class=\"session-title\">{{ session.title }}</span>\n @if (sessionDate(session); as date) {\n <span class=\"session-date\">{{ date }}</span>\n }\n </button>\n @if (assistant.sessionDeleteSupported) {\n <button type=\"button\" class=\"icon-btn session-delete\"\n [attr.aria-label]=\"format(m().deleteSession, { title: session.title })\"\n [title]=\"format(m().deleteSession, { title: session.title })\"\n [disabled]=\"busySession() === session.id\"\n (click)=\"askDelete(session)\">\n <kendo-svg-icon [icon]=\"trashIcon\" size=\"small\"></kendo-svg-icon>\n </button>\n }\n }\n </li>\n } @empty {\n <li class=\"session-empty\">{{ m().noSessions }}</li>\n }\n </ul>\n </section>\n} @else {\n @if (showGreeting()) {\n <div class=\"greeting\" data-testid=\"assistant-greeting\">\n <mm-octobot animation=\"wave\" size=\"md\" playOnce />\n <p class=\"greeting-text\">{{ m().greeting }}</p>\n </div>\n }\n @if (showStarters()) {\n <section class=\"starters\" aria-labelledby=\"assistant-starters-title\">\n <h3 class=\"starters-title\" id=\"assistant-starters-title\">{{ m().startersTitle }}</h3>\n <ul class=\"starter-list\">\n @for (question of assistant.starterQuestions(); track $index) {\n <li><button type=\"button\" class=\"starter\" [disabled]=\"!assistant.canSend()\" (click)=\"sendStarter(question)\">{{ question }}</button></li>\n }\n </ul>\n </section>\n }\n\n <mm-assistant-thread class=\"thread\" [items]=\"assistant.thread()\" [messages]=\"messages()\" (proposalDecision)=\"onDecision($event)\"></mm-assistant-thread>\n\n @if (!connected() || assistant.transportStatus() === 'error') {\n <div class=\"unavailable\" role=\"status\">\n <span class=\"sparkle\" aria-hidden=\"true\">\u2726</span>\n <p>{{ assistant.transportMessage() ?? m().notAvailable }}</p>\n </div>\n }\n\n <mm-assistant-composer\n [messages]=\"messages()\"\n [text]=\"assistant.draft()\"\n (textChange)=\"assistant.setDraft($event)\"\n [attachments]=\"assistant.attachments\"\n [files]=\"assistant.draftFiles()\"\n (filesChange)=\"assistant.setDraftFiles($event)\"\n [canSend]=\"assistant.canSend()\"\n [sending]=\"assistant.sending()\"\n (send)=\"assistant.send($event)\"\n (stop)=\"assistant.stop()\">\n </mm-assistant-composer>\n}\n", styles: [":host{position:relative;display:flex;flex-direction:column;width:400px;min-height:0;border-left:1px solid var(--theme-border-subtle);background:var(--theme-bg-surface);color:var(--theme-text-primary)}.head{display:flex;align-items:center;gap:var(--theme-space-2);padding:10px 14px;border-bottom:1px solid var(--theme-border-subtle)}.title{flex:none;margin:0;white-space:nowrap;font-family:var(--theme-font-display);font-size:.8125rem;font-weight:600}.sparkle{color:var(--theme-ai)}.chip-neutral{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-left:6px;padding:0 7px;border-radius:var(--theme-radius-xs);background:var(--theme-status-neutral-subtle);color:var(--theme-text-secondary);font-size:.71875rem}.icon-btn{flex:none;display:grid;place-items:center;width:28px;height:28px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);cursor:pointer}.icon-btn:hover{background:var(--theme-bg-hover)}.icon-btn[aria-expanded=true]{background:var(--theme-accent-subtle);color:var(--theme-text-primary)}.icon-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.close{margin-left:auto}.ctx{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:8px 14px;list-style:none;border-bottom:1px solid var(--theme-border-subtle)}.ctx-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:22px;padding:0 4px 0 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);font-size:.75rem;color:var(--theme-text-secondary)}.ctx-chip button{display:inline-grid;flex:none;place-items:center;border:0;padding:0 2px;background:none;color:var(--theme-text-muted);cursor:pointer}.ctx-chip button:hover{color:var(--theme-text-primary)}.ctx-chip button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.ctx-label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.thread{flex:1 1 auto;min-height:0}.unavailable{display:flex;align-items:center;gap:var(--theme-space-2);margin:0 14px var(--theme-space-2);padding:8px 10px;border:1px dashed var(--theme-border-default);border-radius:var(--theme-radius-sm);color:var(--theme-text-secondary)}.unavailable p{margin:0}@media(max-width:1180px){:host{width:340px}}@media(max-width:640px){:host{position:fixed;inset:48px 0 0;z-index:30;width:auto;border-left:0}}.head-actions{display:flex;flex:none;gap:2px;margin-left:auto}.head-actions+.close{margin-left:0}.text-btn{height:26px;padding:0 8px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;font-size:.75rem;cursor:pointer}.text-btn:hover:not(:disabled){background:var(--theme-bg-hover);color:var(--theme-text-primary)}.text-btn[aria-expanded=true]{background:var(--theme-accent-subtle);color:var(--theme-text-primary)}.text-btn:disabled{color:var(--theme-text-muted);cursor:default}.text-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.text-btn.danger{color:var(--theme-status-error)}.sessions{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--theme-space-2);min-height:0;padding:10px 14px;overflow:auto}.sessions .back{align-self:flex-start}.session-list{display:grid;gap:2px;margin:0;padding:0;list-style:none}.session-row{display:flex;align-items:center;gap:4px;border-radius:var(--theme-radius-sm)}.session-row.active{background:var(--theme-accent-subtle)}.session-open{display:grid;flex:1 1 auto;min-width:0;padding:6px 8px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;cursor:pointer}.session-open:hover:not(:disabled){background:var(--theme-bg-hover)}.session-open:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.session-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.session-date{color:var(--theme-text-muted);font-size:.71875rem}.session-confirm{display:flex;flex:1 1 auto;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 8px}.confirm-text{flex:1 1 100%;overflow-wrap:anywhere}.session-empty{padding:6px 8px;color:var(--theme-text-secondary)}.greeting{display:grid;justify-items:center;gap:var(--theme-space-2);padding:20px 14px 0;text-align:center}.greeting-text{margin:0;color:var(--theme-text-primary);font-weight:600}.starters{display:grid;gap:var(--theme-space-2);padding:14px 14px 0}.starters-title{margin:0;color:var(--theme-text-secondary);font-size:.75rem;font-weight:600}.starter-list{display:grid;gap:6px;margin:0;padding:0;list-style:none}.starter{width:100%;padding:6px 10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;cursor:pointer}.starter:hover:not(:disabled){border-color:var(--theme-ai);background:var(--theme-bg-hover)}.starter:disabled{color:var(--theme-text-muted);cursor:default}.starter:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.resize-handle{--mm-resize-line: transparent;--mm-resize-grip: var(--theme-border-default, var(--theme-border-subtle, rgba(128, 128, 128, .5)));position:absolute;top:0;bottom:0;left:-4px;z-index:1;width:8px;cursor:col-resize;touch-action:none}.resize-handle:after{content:\"\";position:absolute;top:0;bottom:0;left:3px;width:2px;background:var(--mm-resize-line);transition:background-color .15s}.resize-handle:before{content:\"\";position:absolute;top:50%;left:2px;z-index:1;width:4px;height:32px;margin-top:-16px;border-radius:2px;background:var(--mm-resize-grip);transition:background-color .15s}.resize-handle:hover,.resize-handle:focus-visible{--mm-resize-line: var(--theme-accent);--mm-resize-grip: var(--theme-accent)}.resize-handle:focus-visible{outline:none}:host(.mm-assistant-resizing){-webkit-user-select:none;user-select:none;cursor:col-resize}:host(.mm-assistant-resizing) .resize-handle{--mm-resize-line: var(--theme-accent);--mm-resize-grip: var(--theme-accent)}@media(prefers-reduced-motion:reduce){.resize-handle:after,.resize-handle:before{transition:none}}:host(.mm-assistant-overlay){position:fixed;top:var(--mm-assistant-overlay-top, 48px);right:0;bottom:0;z-index:var(--mm-assistant-overlay-z-index, 20);box-shadow:var(--theme-shadow-panel)}\n"], dependencies: [{ kind: "component", type: AssistantThreadComponent, selector: "mm-assistant-thread", inputs: ["items", "messages"], outputs: ["proposalDecision"] }, { kind: "component", type: AssistantComposerComponent, selector: "mm-assistant-composer", inputs: ["text", "canSend", "sending", "attachments", "files", "messages"], outputs: ["textChange", "filesChange", "send", "stop"] }, { kind: "component", type: SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon", "variant"], exportAs: ["kendoSVGIcon"] }, { kind: "component", type: OctoBotComponent, selector: "mm-octobot", inputs: ["animation", "size", "label", "loading", "still", "playOnce"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
737
+ }
738
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: AssistantPanelComponent, decorators: [{
739
+ type: Component,
740
+ args: [{ selector: 'mm-assistant-panel', imports: [AssistantThreadComponent, AssistantComposerComponent, SVGIconComponent, OctoBotComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
741
+ '[attr.role]': 'isPhone() ? "dialog" : "complementary"',
742
+ '[attr.aria-modal]': 'isPhone() ? "true" : null',
743
+ 'aria-labelledby': 'assistant-panel-title',
744
+ id: 'assistant-panel',
745
+ '[class.mm-assistant-overlay]': 'overlay()',
746
+ '[class.mm-assistant-resizing]': 'resizing()',
747
+ '[attr.data-mode]': 'effectiveMode()',
748
+ '[style.width.px]': 'isPhone() ? null : appliedWidth()',
749
+ '(keydown.escape)': 'onEscape($event)',
750
+ '(keydown.tab)': 'onTab($event)',
751
+ '(keydown.shift.tab)': 'onTab($event)'
752
+ }, template: "@if (canResize()) {\n <div class=\"resize-handle\" role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\" aria-controls=\"assistant-panel\"\n [attr.aria-label]=\"m().resizePanel\" [title]=\"m().resizePanel\"\n [attr.aria-valuenow]=\"currentWidth()\" [attr.aria-valuemin]=\"effectiveMin()\" [attr.aria-valuemax]=\"effectiveMax()\"\n (pointerdown)=\"startResize($event)\" (keydown)=\"onResizeKey($event)\" (dblclick)=\"resetWidth()\"></div>\n}\n\n<header class=\"head\">\n <span class=\"sparkle\" aria-hidden=\"true\">\u2726</span>\n <h2 class=\"title\" id=\"assistant-panel-title\">{{ m().title }}</h2>\n @if (connected()) {\n <span class=\"chip-neutral\" [title]=\"m().readsWithYourPermissions\">{{ m().readsWithYourPermissions }}</span>\n }\n @if (assistant.sessionsSupported) {\n <div class=\"head-actions\">\n <button type=\"button\" class=\"icon-btn new-chat\" [attr.aria-label]=\"m().newChat\" [title]=\"m().newChat\" (click)=\"newChat()\">\n <kendo-svg-icon [icon]=\"plusIcon\"></kendo-svg-icon>\n </button>\n <button type=\"button\" class=\"icon-btn sessions-toggle\" aria-controls=\"assistant-sessions\"\n [attr.aria-label]=\"m().sessions\" [title]=\"m().sessions\"\n [attr.aria-expanded]=\"view() === 'sessions'\"\n (click)=\"view() === 'sessions' ? backToChat() : showSessions()\">\n <kendo-svg-icon [icon]=\"historyIcon\"></kendo-svg-icon>\n </button>\n </div>\n }\n <button type=\"button\" class=\"icon-btn close\" [attr.aria-label]=\"m().closeAssistant\" [title]=\"m().closeAssistant\" (click)=\"assistant.close()\">\n <kendo-svg-icon [icon]=\"xIcon\"></kendo-svg-icon>\n </button>\n</header>\n\n@if (assistant.contextChips().length) {\n <ul class=\"ctx\" [attr.aria-label]=\"m().contextLabel\">\n @for (chip of assistant.contextChips(); track chip.id; let i = $index) {\n <li class=\"ctx-chip\" [attr.data-kind]=\"chip.kind\" [title]=\"chip.value\">\n <span class=\"ctx-label\">{{ chip.label }}</span>\n <button type=\"button\" [attr.aria-label]=\"format(m().removeContext, { label: chip.label })\" [title]=\"format(m().removeContext, { label: chip.label })\" (click)=\"removeChip(chip.id, i)\"><kendo-svg-icon [icon]=\"xIcon\" size=\"small\"></kendo-svg-icon></button>\n </li>\n }\n </ul>\n}\n\n@if (view() === 'sessions') {\n <section class=\"sessions\" id=\"assistant-sessions\" [attr.aria-label]=\"m().sessionsLabel\">\n <button type=\"button\" class=\"text-btn back\" (click)=\"backToChat()\">\u2190 {{ m().backToChat }}</button>\n <ul class=\"session-list\">\n @for (session of assistant.sessions(); track session.id) {\n <li class=\"session-row\" [attr.data-session-id]=\"session.id\"\n [class.active]=\"session.id === assistant.activeSessionId()\">\n @if (confirmingDelete() === session.id) {\n <div class=\"session-confirm\" role=\"group\" [attr.aria-label]=\"format(m().deleteSessionConfirm, { title: session.title })\">\n <span class=\"confirm-text\">{{ format(m().deleteSessionConfirm, { title: session.title }) }}</span>\n <button type=\"button\" class=\"text-btn danger confirm-delete\" [disabled]=\"busySession() === session.id\"\n (click)=\"confirmDelete(session)\">{{ m().delete }}</button>\n <button type=\"button\" class=\"text-btn cancel-delete\" (click)=\"cancelDelete()\">{{ m().cancel }}</button>\n </div>\n } @else {\n <button type=\"button\" class=\"session-open\" [title]=\"session.title\"\n [attr.aria-current]=\"session.id === assistant.activeSessionId() ? 'true' : null\"\n [disabled]=\"busySession() === session.id\"\n (click)=\"openSession(session)\">\n <span class=\"session-title\">{{ session.title }}</span>\n @if (sessionDate(session); as date) {\n <span class=\"session-date\">{{ date }}</span>\n }\n </button>\n @if (assistant.sessionDeleteSupported) {\n <button type=\"button\" class=\"icon-btn session-delete\"\n [attr.aria-label]=\"format(m().deleteSession, { title: session.title })\"\n [title]=\"format(m().deleteSession, { title: session.title })\"\n [disabled]=\"busySession() === session.id\"\n (click)=\"askDelete(session)\">\n <kendo-svg-icon [icon]=\"trashIcon\" size=\"small\"></kendo-svg-icon>\n </button>\n }\n }\n </li>\n } @empty {\n <li class=\"session-empty\">{{ m().noSessions }}</li>\n }\n </ul>\n </section>\n} @else {\n @if (showGreeting()) {\n <div class=\"greeting\" data-testid=\"assistant-greeting\">\n <mm-octobot animation=\"wave\" size=\"md\" playOnce />\n <p class=\"greeting-text\">{{ m().greeting }}</p>\n </div>\n }\n @if (showStarters()) {\n <section class=\"starters\" aria-labelledby=\"assistant-starters-title\">\n <h3 class=\"starters-title\" id=\"assistant-starters-title\">{{ m().startersTitle }}</h3>\n <ul class=\"starter-list\">\n @for (question of assistant.starterQuestions(); track $index) {\n <li><button type=\"button\" class=\"starter\" [disabled]=\"!assistant.canSend()\" (click)=\"sendStarter(question)\">{{ question }}</button></li>\n }\n </ul>\n </section>\n }\n\n <mm-assistant-thread class=\"thread\" [items]=\"assistant.thread()\" [messages]=\"messages()\" (proposalDecision)=\"onDecision($event)\"></mm-assistant-thread>\n\n @if (!connected() || assistant.transportStatus() === 'error') {\n <div class=\"unavailable\" role=\"status\">\n <span class=\"sparkle\" aria-hidden=\"true\">\u2726</span>\n <p>{{ assistant.transportMessage() ?? m().notAvailable }}</p>\n </div>\n }\n\n <mm-assistant-composer\n [messages]=\"messages()\"\n [text]=\"assistant.draft()\"\n (textChange)=\"assistant.setDraft($event)\"\n [attachments]=\"assistant.attachments\"\n [files]=\"assistant.draftFiles()\"\n (filesChange)=\"assistant.setDraftFiles($event)\"\n [canSend]=\"assistant.canSend()\"\n [sending]=\"assistant.sending()\"\n (send)=\"assistant.send($event)\"\n (stop)=\"assistant.stop()\">\n </mm-assistant-composer>\n}\n", styles: [":host{position:relative;display:flex;flex-direction:column;width:400px;min-height:0;border-left:1px solid var(--theme-border-subtle);background:var(--theme-bg-surface);color:var(--theme-text-primary)}.head{display:flex;align-items:center;gap:var(--theme-space-2);padding:10px 14px;border-bottom:1px solid var(--theme-border-subtle)}.title{flex:none;margin:0;white-space:nowrap;font-family:var(--theme-font-display);font-size:.8125rem;font-weight:600}.sparkle{color:var(--theme-ai)}.chip-neutral{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-left:6px;padding:0 7px;border-radius:var(--theme-radius-xs);background:var(--theme-status-neutral-subtle);color:var(--theme-text-secondary);font-size:.71875rem}.icon-btn{flex:none;display:grid;place-items:center;width:28px;height:28px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);cursor:pointer}.icon-btn:hover{background:var(--theme-bg-hover)}.icon-btn[aria-expanded=true]{background:var(--theme-accent-subtle);color:var(--theme-text-primary)}.icon-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.close{margin-left:auto}.ctx{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:8px 14px;list-style:none;border-bottom:1px solid var(--theme-border-subtle)}.ctx-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:22px;padding:0 4px 0 8px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-xs);font-size:.75rem;color:var(--theme-text-secondary)}.ctx-chip button{display:inline-grid;flex:none;place-items:center;border:0;padding:0 2px;background:none;color:var(--theme-text-muted);cursor:pointer}.ctx-chip button:hover{color:var(--theme-text-primary)}.ctx-chip button:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.ctx-label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.thread{flex:1 1 auto;min-height:0}.unavailable{display:flex;align-items:center;gap:var(--theme-space-2);margin:0 14px var(--theme-space-2);padding:8px 10px;border:1px dashed var(--theme-border-default);border-radius:var(--theme-radius-sm);color:var(--theme-text-secondary)}.unavailable p{margin:0}@media(max-width:1180px){:host{width:340px}}@media(max-width:640px){:host{position:fixed;inset:48px 0 0;z-index:30;width:auto;border-left:0}}.head-actions{display:flex;flex:none;gap:2px;margin-left:auto}.head-actions+.close{margin-left:0}.text-btn{height:26px;padding:0 8px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-secondary);font:inherit;font-size:.75rem;cursor:pointer}.text-btn:hover:not(:disabled){background:var(--theme-bg-hover);color:var(--theme-text-primary)}.text-btn[aria-expanded=true]{background:var(--theme-accent-subtle);color:var(--theme-text-primary)}.text-btn:disabled{color:var(--theme-text-muted);cursor:default}.text-btn:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.text-btn.danger{color:var(--theme-status-error)}.sessions{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--theme-space-2);min-height:0;padding:10px 14px;overflow:auto}.sessions .back{align-self:flex-start}.session-list{display:grid;gap:2px;margin:0;padding:0;list-style:none}.session-row{display:flex;align-items:center;gap:4px;border-radius:var(--theme-radius-sm)}.session-row.active{background:var(--theme-accent-subtle)}.session-open{display:grid;flex:1 1 auto;min-width:0;padding:6px 8px;border:0;border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;cursor:pointer}.session-open:hover:not(:disabled){background:var(--theme-bg-hover)}.session-open:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.session-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.session-date{color:var(--theme-text-muted);font-size:.71875rem}.session-confirm{display:flex;flex:1 1 auto;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 8px}.confirm-text{flex:1 1 100%;overflow-wrap:anywhere}.session-empty{padding:6px 8px;color:var(--theme-text-secondary)}.greeting{display:grid;justify-items:center;gap:var(--theme-space-2);padding:20px 14px 0;text-align:center}.greeting-text{margin:0;color:var(--theme-text-primary);font-weight:600}.starters{display:grid;gap:var(--theme-space-2);padding:14px 14px 0}.starters-title{margin:0;color:var(--theme-text-secondary);font-size:.75rem;font-weight:600}.starter-list{display:grid;gap:6px;margin:0;padding:0;list-style:none}.starter{width:100%;padding:6px 10px;border:1px solid var(--theme-border-default);border-radius:var(--theme-radius-sm);background:none;color:var(--theme-text-primary);font:inherit;text-align:left;cursor:pointer}.starter:hover:not(:disabled){border-color:var(--theme-ai);background:var(--theme-bg-hover)}.starter:disabled{color:var(--theme-text-muted);cursor:default}.starter:focus-visible{outline:none;box-shadow:var(--theme-focus-ring)}.resize-handle{--mm-resize-line: transparent;--mm-resize-grip: var(--theme-border-default, var(--theme-border-subtle, rgba(128, 128, 128, .5)));position:absolute;top:0;bottom:0;left:-4px;z-index:1;width:8px;cursor:col-resize;touch-action:none}.resize-handle:after{content:\"\";position:absolute;top:0;bottom:0;left:3px;width:2px;background:var(--mm-resize-line);transition:background-color .15s}.resize-handle:before{content:\"\";position:absolute;top:50%;left:2px;z-index:1;width:4px;height:32px;margin-top:-16px;border-radius:2px;background:var(--mm-resize-grip);transition:background-color .15s}.resize-handle:hover,.resize-handle:focus-visible{--mm-resize-line: var(--theme-accent);--mm-resize-grip: var(--theme-accent)}.resize-handle:focus-visible{outline:none}:host(.mm-assistant-resizing){-webkit-user-select:none;user-select:none;cursor:col-resize}:host(.mm-assistant-resizing) .resize-handle{--mm-resize-line: var(--theme-accent);--mm-resize-grip: var(--theme-accent)}@media(prefers-reduced-motion:reduce){.resize-handle:after,.resize-handle:before{transition:none}}:host(.mm-assistant-overlay){position:fixed;top:var(--mm-assistant-overlay-top, 48px);right:0;bottom:0;z-index:var(--mm-assistant-overlay-z-index, 20);box-shadow:var(--theme-shadow-panel)}\n"] }]
753
+ }], ctorParameters: () => [], propDecorators: { composer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AssistantComposerComponent), { isSignal: true }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }] } });
754
+
755
+ /*
756
+ * Public API Surface of @meshmakers/shared-ui/assistant (AB#5621)
757
+ */
758
+ /**
759
+ * The assistant side panel of the OctoMesh Refinery Studio as an app-agnostic building block:
760
+ * panel (header, removable context chips, thread, composer with slash commands), proposal cards,
761
+ * "✦ Explain" directives and the {@link AssistantService} state. It never answers by itself —
762
+ * turns come from an {@link AssistantTransport} the host provides.
763
+ *
764
+ * Usage:
765
+ *
766
+ * ```ts
767
+ * import { AssistantPanelComponent, AssistantService, ASSISTANT_ENABLED, ASSISTANT_TRANSPORT,
768
+ * assistantHotkey } from '@meshmakers/shared-ui/assistant';
769
+ *
770
+ * // app.config.ts — off by default; without a transport the panel says "backend not available"
771
+ * providers: [
772
+ * { provide: ASSISTANT_ENABLED, useValue: true },
773
+ * { provide: ASSISTANT_TRANSPORT, useExisting: MyChatTransport },
774
+ * ]
775
+ *
776
+ * // AppComponent
777
+ * template: `@if (assistant.enabled && assistant.isOpen()) { <mm-assistant-panel /> }`
778
+ * @HostListener('document:keydown', ['$event'])
779
+ * onKeydown(event: KeyboardEvent): void {
780
+ * if (this.assistant.enabled && assistantHotkey(event)) { event.preventDefault(); this.assistant.toggle(); }
781
+ * }
782
+ * ```
783
+ *
784
+ * Wire the top bar toggle of `@meshmakers/shared-ui/shell` with
785
+ * `[assistantAvailable]="assistant.enabled" [assistantOpen]="assistant.isOpen()" (assistantToggle)="assistant.toggle()"`.
786
+ * Proposal decisions (Run / Edit / Discard) are only published on `AssistantService.proposalDecisions$`.
787
+ *
788
+ * Tokens:
789
+ * - `ASSISTANT_ENABLED` (default `false`) — the single feature flag;
790
+ * - `ASSISTANT_TRANSPORT` (default {@link NotConnectedAssistantTransport}) — where turns come from;
791
+ * - `ASSISTANT_PAGE_CONTEXT` (optional) — area/tab labels for the page chip;
792
+ * - `ASSISTANT_TOOL_CALL_COMPONENT` (default {@link AssistantToolCallRowComponent}) — tool-call renderer
793
+ * (OctoMesh hosts: `AiToolCallComponent` of `@meshmakers/octo-ai-console`);
794
+ * - `ASSISTANT_SLASH_COMMANDS` (default {@link DEFAULT_ASSISTANT_SLASH_COMMANDS}) — composer commands; a
795
+ * command with `expand(command, args)` is sent (and shown) as the prompt it returns (AB#5621);
796
+ * - `ASSISTANT_PANEL_OPTIONS` (optional) — panel layout: `mode` (`docked` | `overlay`), `resizable` (off by default),
797
+ * `minWidth` / `maxWidth`, `storageKey` of the persisted width (AB#5621);
798
+ * - `ASSISTANT_MESSAGES` — translations (`Partial<AssistantMessages>`; component `messages` inputs win).
799
+ *
800
+ * Requires the `marked` package (markdown of assistant turns).
801
+ *
802
+ * The startup-needed, `marked`-free part (service, hotkey, tokens, transports, models, messages,
803
+ * Explain directives) lives in `@meshmakers/shared-ui/assistant-core` and is re-exported here with
804
+ * the same identity. Import from `assistant-core` in eagerly loaded code and load the panel lazily.
805
+ * {@link FakeAssistantTransport} is a scriptable transport for host specs.
806
+ *
807
+ * Optional transport capabilities (AB#5621) — the panel adds UI only for what the transport has:
808
+ * `sessions` + `loadSession` (+ `refreshSessions`, `deleteSession`, `newSession`, `activeSessionId`)
809
+ * → "New chat" / "Chats" list; `attachments` → composer file picker, files on
810
+ * `AssistantSendRequest.files`; `starterQuestions()` → clickable questions on an empty thread.
811
+ */
812
+ // Startup part — re-exported from assistant-core so tokens and classes keep a single identity.
813
+
814
+ /**
815
+ * Generated bundle index. Do not edit.
816
+ */
817
+
818
+ export { AssistantComposerComponent, AssistantMarkdownPipe, AssistantPanelComponent, AssistantProposalCardComponent, AssistantThreadComponent, classifyAssistantHref, renderAssistantMarkdown };
819
+ //# sourceMappingURL=meshmakers-shared-ui-assistant.mjs.map