@meshmakers/octo-ai-console 3.3.1210 → 3.4.70

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.
@@ -2,9 +2,9 @@ import * as i0 from '@angular/core';
2
2
  import { inject, Injectable, InjectionToken, makeEnvironmentProviders, input, computed, ChangeDetectionStrategy, Component, output, signal } from '@angular/core';
3
3
  import { HttpClient, HttpParams } from '@angular/common/http';
4
4
  import { ReplaySubject, Subject } from 'rxjs';
5
+ import { CommonModule } from '@angular/common';
5
6
  import * as i1 from '@angular/forms';
6
7
  import { FormsModule } from '@angular/forms';
7
- import { CommonModule } from '@angular/common';
8
8
 
9
9
  /**
10
10
  * Typed REST client for the AI Adapter's session endpoints. Every method maps
@@ -61,6 +61,35 @@ class AiAdapterClientService {
61
61
  cancelSession(sessionId) {
62
62
  return this.http.post(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}/cancel`, null);
63
63
  }
64
+ /**
65
+ * `DELETE /{tenantId}/v1/sessions/{sessionId}` — hard-delete a terminal
66
+ * session. The server refuses non-terminal sessions with HTTP 409 and the
67
+ * <code>invalid_state_transition</code> error code, so callers should cancel
68
+ * first when the session is still in flight. 204 on success; the response
69
+ * has no body.
70
+ */
71
+ deleteSession(sessionId) {
72
+ return this.http.delete(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}`);
73
+ }
74
+ /**
75
+ * `POST /{tenantId}/v1/sessions/{sessionId}/messages` — append a follow-up
76
+ * user turn to a `Completed` session. The adapter re-materialises the
77
+ * workspace (fresh credential snapshot), recovers Claude's session id from
78
+ * the persisted `system/init` event of the original turn, and spawns
79
+ * <code>claude --resume &lt;id&gt;</code> so the conversation context
80
+ * carries over. The next turn's events stream on the same SignalR channel
81
+ * as the prior turn — there is no separate subscription to set up.
82
+ *
83
+ * Server response is 202 with the updated session DTO (status flipped back
84
+ * to `Running`); the caller can use the returned status to drive an
85
+ * optimistic UI update. Non-Completed sessions return 409 with
86
+ * <code>invalid_state_transition</code>; a session whose first turn died
87
+ * before claude emitted init returns 409 with
88
+ * <code>claude_session_id_not_captured</code>.
89
+ */
90
+ sendMessage(sessionId, text) {
91
+ return this.http.post(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}/messages`, { text });
92
+ }
64
93
  /**
65
94
  * `GET /{tenantId}/v1/sessions/{sessionId}/events?sinceSequence=N` — replay
66
95
  * persisted events for a session. UI uses this on reconnect to backfill any
@@ -95,6 +124,40 @@ class AiAdapterClientService {
95
124
  issueCredentialTicket(request) {
96
125
  return this.http.post(`${this.tenantBase()}/credentials/tickets`, request);
97
126
  }
127
+ /**
128
+ * `POST /{tenantId}/v1/credentials/github-pat` — register a GitHub Personal
129
+ * Access Token (#4124). The adapter validates the token against GitHub's
130
+ * `/user` endpoint before encryption; a refused token surfaces as HTTP 422
131
+ * with `error: 'github_refused_token'`. The response is intentionally minimal
132
+ * (`{ rtId }`); the host re-fetches the list so the rendering path stays
133
+ * shared with refresh.
134
+ */
135
+ registerGitHubPat(request) {
136
+ return this.http.post(`${this.tenantBase()}/credentials/github-pat`, request);
137
+ }
138
+ /**
139
+ * `GET /{tenantId}/v1/credentials/github-pat` — list registered PATs with a
140
+ * masked tail + GitHub login. Plaintext never crosses the wire.
141
+ */
142
+ listGitHubPats() {
143
+ return this.http.get(`${this.tenantBase()}/credentials/github-pat`);
144
+ }
145
+ /**
146
+ * `DELETE /{tenantId}/v1/credentials/github-pat/{rtId}` — revoke a PAT. The
147
+ * ciphertext is deleted, not soft-tombstoned. Returns void on 204.
148
+ */
149
+ deleteGitHubPat(rtId) {
150
+ return this.http.delete(`${this.tenantBase()}/credentials/github-pat/${encodeURIComponent(rtId)}`);
151
+ }
152
+ /**
153
+ * `GET /{tenantId}/v1/app-templates` — list the Custom-App templates the
154
+ * adapter exposes for the wizard (#4145). Phase-1 returns the same three
155
+ * static entries for every tenant; the wire shape is stable so per-tenant
156
+ * overrides can ride it without an SPA bump.
157
+ */
158
+ getAppTemplates() {
159
+ return this.http.get(`${this.tenantBase()}/app-templates`);
160
+ }
98
161
  tenantBase() {
99
162
  return `${this.options.baseUrl}/${encodeURIComponent(this.options.tenantId)}/v1`;
100
163
  }
@@ -333,15 +396,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
333
396
  * the component takes the array as an input signal and emits the rtId on
334
397
  * row click. Keeping it data-source-agnostic lets refinery wire its own
335
398
  * Apollo cache while a bastion CLI could pass static fixtures in a Storybook.
399
+ *
400
+ * Cancel and Delete are surfaced as inline per-row actions and emitted as
401
+ * separate outputs — the host owns the confirmation dialog and the actual
402
+ * REST call. The component only knows which actions are valid for which
403
+ * status, so a Running session never offers Delete and a Completed session
404
+ * never offers Cancel.
336
405
  */
337
406
  class AiSessionListComponent {
338
407
  sessions = input.required(...(ngDevMode ? [{ debugName: "sessions" }] : /* istanbul ignore next */ []));
339
408
  selectedSessionId = input(null, ...(ngDevMode ? [{ debugName: "selectedSessionId" }] : /* istanbul ignore next */ []));
340
409
  sessionSelected = output();
410
+ cancelRequested = output();
411
+ deleteRequested = output();
341
412
  orderedSessions = computed(() => [...this.sessions()].sort((a, b) => b.startedAt.localeCompare(a.startedAt)), ...(ngDevMode ? [{ debugName: "orderedSessions" }] : /* istanbul ignore next */ []));
413
+ /**
414
+ * Terminal status set, kept in sync with the C# `AiAgentSessionService.IsTerminal`
415
+ * predicate. Delete is gated to this set; Cancel is gated to its complement.
416
+ * If the wire enum grows a new terminal kind, both predicates must be
417
+ * updated here AND on the server.
418
+ */
419
+ static TERMINAL_STATUSES = new Set(['Completed', 'Failed', 'Cancelled']);
420
+ canCancel(status) {
421
+ return !AiSessionListComponent.TERMINAL_STATUSES.has(status);
422
+ }
423
+ canDelete(status) {
424
+ return AiSessionListComponent.TERMINAL_STATUSES.has(status);
425
+ }
342
426
  onSelect(sessionRtId) {
343
427
  this.sessionSelected.emit(sessionRtId);
344
428
  }
429
+ onCancel(sessionRtId, event) {
430
+ // Without stopPropagation the row click also fires and selects this
431
+ // session, which then redirects focus and tears the inline-action button
432
+ // out from under the user mid-confirmation.
433
+ event.stopPropagation();
434
+ this.cancelRequested.emit(sessionRtId);
435
+ }
436
+ onDelete(sessionRtId, event) {
437
+ event.stopPropagation();
438
+ this.deleteRequested.emit(sessionRtId);
439
+ }
345
440
  age(startedAt) {
346
441
  const started = Date.parse(startedAt);
347
442
  if (Number.isNaN(started)) {
@@ -364,56 +459,419 @@ class AiSessionListComponent {
364
459
  return `${days}d ago`;
365
460
  }
366
461
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
367
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiSessionListComponent, isStandalone: true, selector: "mm-ai-session-list", inputs: { sessions: { classPropertyName: "sessions", publicName: "sessions", isSignal: true, isRequired: true, transformFunction: null }, selectedSessionId: { classPropertyName: "selectedSessionId", publicName: "selectedSessionId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionSelected: "sessionSelected" }, ngImport: i0, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <div class=\"mm-ai-session-list__main\">\n <div class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</div>\n <div class=\"mm-ai-session-list__meta\">\n <span class=\"mm-ai-session-list__owner\">{{ session.ownerUserId }}</span>\n <span class=\"mm-ai-session-list__age\">{{ age(session.startedAt) }}</span>\n <span class=\"mm-ai-session-list__tokens\">\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the worker\n pod's nginx sidecar has a build to serve. Opens in a new tab so\n the operator's session-list scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Open preview\n </a>\n }\n </div>\n </div>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__main{flex:1;min-width:0}.mm-ai-session-list__goal{font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__meta{display:flex;gap:.75rem;margin-top:.15rem;font-size:.75rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.75rem;text-decoration:underline;text-underline-offset:2px}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}\n"], dependencies: [{ kind: "component", type: AiJobStatusBadgeComponent, selector: "mm-ai-job-status-badge", inputs: ["status"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
462
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiSessionListComponent, isStandalone: true, selector: "mm-ai-session-list", inputs: { sessions: { classPropertyName: "sessions", publicName: "sessions", isSignal: true, isRequired: true, transformFunction: null }, selectedSessionId: { classPropertyName: "selectedSessionId", publicName: "selectedSessionId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionSelected: "sessionSelected", cancelRequested: "cancelRequested", deleteRequested: "deleteRequested" }, ngImport: i0, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <!--\n Row layout: goal + status badge on top, meta + actions on the\n bottom. The two-line shape lets a narrow sidebar (Refinery\n Studio caps the column at 18rem) breathe \u2014 the previous\n horizontal cram had the goal text overlap the action buttons\n on every row.\n -->\n <div class=\"mm-ai-session-list__top\">\n <span class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</span>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </div>\n <div class=\"mm-ai-session-list__bottom\">\n <span class=\"mm-ai-session-list__meta\">\n {{ age(session.startedAt) }}\n <span class=\"mm-ai-session-list__dot\">\u00B7</span>\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the\n worker pod's nginx sidecar has a build to serve. Opens in\n a new tab so the sidebar scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Preview\n </a>\n }\n <span class=\"mm-ai-session-list__spacer\"></span>\n <span class=\"mm-ai-session-list__actions\">\n @if (canCancel(session.status)) {\n <button\n type=\"button\"\n class=\"mm-ai-session-list__action mm-ai-session-list__action--cancel\"\n [attr.aria-label]=\"'Cancel session ' + session.goalSummary\"\n (click)=\"onCancel(session.sessionRtId, $event)\"\n >\n Cancel\n </button>\n }\n @if (canDelete(session.status)) {\n <button\n type=\"button\"\n class=\"mm-ai-session-list__action mm-ai-session-list__action--delete\"\n [attr.aria-label]=\"'Delete session ' + session.goalSummary\"\n (click)=\"onDelete(session.sessionRtId, $event)\"\n >\n Delete\n </button>\n }\n </span>\n </div>\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9;--mm-ai-list-action-cancel: #da9162;--mm-ai-list-action-delete: #ec658f}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;flex-direction:column;gap:.3rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit;min-width:0}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__top{display:flex;align-items:center;gap:.5rem;min-width:0}.mm-ai-session-list__goal{flex:1;min-width:0;font-size:.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__bottom{display:flex;align-items:center;gap:.5rem;font-size:.7rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums;min-width:0}.mm-ai-session-list__meta{display:inline-flex;align-items:baseline;gap:.35rem;flex-shrink:0}.mm-ai-session-list__dot{opacity:.5}.mm-ai-session-list__spacer{flex:1}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.7rem;text-decoration:underline;text-underline-offset:2px;flex-shrink:0}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}.mm-ai-session-list__actions{display:flex;gap:.25rem;flex-shrink:0;opacity:.45;transition:opacity .15s ease}.mm-ai-session-list__row:hover .mm-ai-session-list__actions,.mm-ai-session-list__row--selected .mm-ai-session-list__actions{opacity:1}.mm-ai-session-list__action{padding:.15rem .5rem;background:transparent;border:1px solid var(--mm-ai-list-border);border-radius:.25rem;color:var(--mm-ai-list-text);font-family:inherit;font-size:.65rem;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:border-color .15s ease,color .15s ease,background-color .15s ease}.mm-ai-session-list__action:hover{background:#ffffff0a}.mm-ai-session-list__action--cancel{color:var(--mm-ai-list-action-cancel);border-color:color-mix(in srgb,var(--mm-ai-list-action-cancel) 40%,transparent)}.mm-ai-session-list__action--cancel:hover{border-color:var(--mm-ai-list-action-cancel)}.mm-ai-session-list__action--delete{color:var(--mm-ai-list-action-delete);border-color:color-mix(in srgb,var(--mm-ai-list-action-delete) 40%,transparent)}.mm-ai-session-list__action--delete:hover{border-color:var(--mm-ai-list-action-delete)}\n"], dependencies: [{ kind: "component", type: AiJobStatusBadgeComponent, selector: "mm-ai-job-status-badge", inputs: ["status"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
368
463
  }
369
464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionListComponent, decorators: [{
370
465
  type: Component,
371
- args: [{ selector: 'mm-ai-session-list', imports: [AiJobStatusBadgeComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <div class=\"mm-ai-session-list__main\">\n <div class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</div>\n <div class=\"mm-ai-session-list__meta\">\n <span class=\"mm-ai-session-list__owner\">{{ session.ownerUserId }}</span>\n <span class=\"mm-ai-session-list__age\">{{ age(session.startedAt) }}</span>\n <span class=\"mm-ai-session-list__tokens\">\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the worker\n pod's nginx sidecar has a build to serve. Opens in a new tab so\n the operator's session-list scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Open preview\n </a>\n }\n </div>\n </div>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__main{flex:1;min-width:0}.mm-ai-session-list__goal{font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__meta{display:flex;gap:.75rem;margin-top:.15rem;font-size:.75rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.75rem;text-decoration:underline;text-underline-offset:2px}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}\n"] }]
372
- }], propDecorators: { sessions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessions", required: true }] }], selectedSessionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedSessionId", required: false }] }], sessionSelected: [{ type: i0.Output, args: ["sessionSelected"] }] } });
466
+ args: [{ selector: 'mm-ai-session-list', imports: [AiJobStatusBadgeComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <!--\n Row layout: goal + status badge on top, meta + actions on the\n bottom. The two-line shape lets a narrow sidebar (Refinery\n Studio caps the column at 18rem) breathe \u2014 the previous\n horizontal cram had the goal text overlap the action buttons\n on every row.\n -->\n <div class=\"mm-ai-session-list__top\">\n <span class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</span>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </div>\n <div class=\"mm-ai-session-list__bottom\">\n <span class=\"mm-ai-session-list__meta\">\n {{ age(session.startedAt) }}\n <span class=\"mm-ai-session-list__dot\">\u00B7</span>\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the\n worker pod's nginx sidecar has a build to serve. Opens in\n a new tab so the sidebar scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Preview\n </a>\n }\n <span class=\"mm-ai-session-list__spacer\"></span>\n <span class=\"mm-ai-session-list__actions\">\n @if (canCancel(session.status)) {\n <button\n type=\"button\"\n class=\"mm-ai-session-list__action mm-ai-session-list__action--cancel\"\n [attr.aria-label]=\"'Cancel session ' + session.goalSummary\"\n (click)=\"onCancel(session.sessionRtId, $event)\"\n >\n Cancel\n </button>\n }\n @if (canDelete(session.status)) {\n <button\n type=\"button\"\n class=\"mm-ai-session-list__action mm-ai-session-list__action--delete\"\n [attr.aria-label]=\"'Delete session ' + session.goalSummary\"\n (click)=\"onDelete(session.sessionRtId, $event)\"\n >\n Delete\n </button>\n }\n </span>\n </div>\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9;--mm-ai-list-action-cancel: #da9162;--mm-ai-list-action-delete: #ec658f}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;flex-direction:column;gap:.3rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit;min-width:0}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__top{display:flex;align-items:center;gap:.5rem;min-width:0}.mm-ai-session-list__goal{flex:1;min-width:0;font-size:.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__bottom{display:flex;align-items:center;gap:.5rem;font-size:.7rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums;min-width:0}.mm-ai-session-list__meta{display:inline-flex;align-items:baseline;gap:.35rem;flex-shrink:0}.mm-ai-session-list__dot{opacity:.5}.mm-ai-session-list__spacer{flex:1}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.7rem;text-decoration:underline;text-underline-offset:2px;flex-shrink:0}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}.mm-ai-session-list__actions{display:flex;gap:.25rem;flex-shrink:0;opacity:.45;transition:opacity .15s ease}.mm-ai-session-list__row:hover .mm-ai-session-list__actions,.mm-ai-session-list__row--selected .mm-ai-session-list__actions{opacity:1}.mm-ai-session-list__action{padding:.15rem .5rem;background:transparent;border:1px solid var(--mm-ai-list-border);border-radius:.25rem;color:var(--mm-ai-list-text);font-family:inherit;font-size:.65rem;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:border-color .15s ease,color .15s ease,background-color .15s ease}.mm-ai-session-list__action:hover{background:#ffffff0a}.mm-ai-session-list__action--cancel{color:var(--mm-ai-list-action-cancel);border-color:color-mix(in srgb,var(--mm-ai-list-action-cancel) 40%,transparent)}.mm-ai-session-list__action--cancel:hover{border-color:var(--mm-ai-list-action-cancel)}.mm-ai-session-list__action--delete{color:var(--mm-ai-list-action-delete);border-color:color-mix(in srgb,var(--mm-ai-list-action-delete) 40%,transparent)}.mm-ai-session-list__action--delete:hover{border-color:var(--mm-ai-list-action-delete)}\n"] }]
467
+ }], propDecorators: { sessions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessions", required: true }] }], selectedSessionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedSessionId", required: false }] }], sessionSelected: [{ type: i0.Output, args: ["sessionSelected"] }], cancelRequested: [{ type: i0.Output, args: ["cancelRequested"] }], deleteRequested: [{ type: i0.Output, args: ["deleteRequested"] }] } });
373
468
 
374
469
  /**
375
- * Read-only transcript of a session, derived from the events stream. The
376
- * component takes the events as an input signal so the host owns lifetime —
377
- * a parent that's already wired `AiSessionStreamService.events$` into a
378
- * `toSignal()` just hands the same signal in.
470
+ * Parse one persisted session event into a `ParsedView`. Falls back to
471
+ * `unknown` (never throws) when the payload is malformed we'd rather show
472
+ * "unrecognised event" than crash the whole transcript.
473
+ */
474
+ function parseSessionEvent(event) {
475
+ const payload = safeJsonParse(event.payload);
476
+ if (payload === parseFailure) {
477
+ return {
478
+ event,
479
+ view: {
480
+ kind: 'unknown',
481
+ summary: 'Malformed event payload',
482
+ raw: event.payload,
483
+ },
484
+ };
485
+ }
486
+ if (event.kind === 'StatusChange' && isRecord(payload)) {
487
+ // The orchestrator emits a StatusChange wrapper around any non-content
488
+ // worker event — system/init, hooks, rate-limit, etc. Demote based on the
489
+ // wrapped `type` so the renderer can collapse the noise.
490
+ if (isClaudeStreamFrame(payload)) {
491
+ return { event, view: viewFromClaudeFrame(payload) };
492
+ }
493
+ const status = readString(payload, 'newStatus') ?? readString(payload, 'status');
494
+ if (status) {
495
+ return { event, view: { kind: 'status-change', status, raw: payload } };
496
+ }
497
+ }
498
+ if (event.kind === 'Hook' && isRecord(payload)) {
499
+ return { event, view: viewSystemHook(payload) };
500
+ }
501
+ if (event.kind === 'Message' && isRecord(payload)) {
502
+ if (isClaudeStreamFrame(payload)) {
503
+ return { event, view: viewFromClaudeFrame(payload) };
504
+ }
505
+ // Bare text Message — promote to a single-part assistant bubble so the
506
+ // visual treatment is consistent.
507
+ const text = readString(payload, 'text');
508
+ if (text) {
509
+ return {
510
+ event,
511
+ view: {
512
+ kind: 'assistant',
513
+ parts: [{ type: 'text', text }],
514
+ model: null,
515
+ usage: null,
516
+ raw: payload,
517
+ },
518
+ };
519
+ }
520
+ }
521
+ if (event.kind === 'ToolCall' && isRecord(payload)) {
522
+ return {
523
+ event,
524
+ view: {
525
+ kind: 'assistant',
526
+ parts: [
527
+ {
528
+ type: 'tool_use',
529
+ id: readString(payload, 'callId') ?? '',
530
+ toolName: readString(payload, 'toolName') ?? 'tool',
531
+ input: payload['arguments'] ?? null,
532
+ },
533
+ ],
534
+ model: null,
535
+ usage: null,
536
+ raw: payload,
537
+ },
538
+ };
539
+ }
540
+ if (event.kind === 'ToolResult' && isRecord(payload)) {
541
+ return {
542
+ event,
543
+ view: {
544
+ kind: 'tool-result',
545
+ toolUseId: readString(payload, 'callId') ?? readString(payload, 'toolUseId') ?? '',
546
+ content: stringifyToolContent(payload['result'] ?? payload['content']),
547
+ isError: readBoolean(payload, 'isError') ?? false,
548
+ raw: payload,
549
+ },
550
+ };
551
+ }
552
+ if (event.kind === 'Error') {
553
+ return {
554
+ event,
555
+ view: {
556
+ kind: 'unknown',
557
+ summary: typeof payload === 'string' ? payload : 'Error event',
558
+ raw: payload,
559
+ },
560
+ };
561
+ }
562
+ return {
563
+ event,
564
+ view: { kind: 'unknown', summary: `Unhandled kind: ${event.kind}`, raw: payload },
565
+ };
566
+ }
567
+ /**
568
+ * Sort parsed events by sequence so out-of-order arrival (the REST backfill +
569
+ * live SignalR union) renders deterministically. Kept here so the component
570
+ * doesn't redo the sort logic every render cycle.
571
+ */
572
+ function parseAndOrder(events) {
573
+ return [...events]
574
+ .sort((a, b) => a.sequence - b.sequence)
575
+ .map(parseSessionEvent);
576
+ }
577
+ // ────────────────────────────────────────────────────────────────────────────
578
+ // Internals
579
+ // ────────────────────────────────────────────────────────────────────────────
580
+ const parseFailure = Symbol('parseFailure');
581
+ function safeJsonParse(raw) {
582
+ try {
583
+ return JSON.parse(raw);
584
+ }
585
+ catch {
586
+ return parseFailure;
587
+ }
588
+ }
589
+ function isRecord(v) {
590
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
591
+ }
592
+ function readString(rec, key) {
593
+ const v = rec[key];
594
+ return typeof v === 'string' && v.length > 0 ? v : null;
595
+ }
596
+ function readNumber(rec, key) {
597
+ const v = rec[key];
598
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
599
+ }
600
+ function readBoolean(rec, key) {
601
+ const v = rec[key];
602
+ return typeof v === 'boolean' ? v : null;
603
+ }
604
+ /**
605
+ * Claude Code's stream-json shapes a frame as `{ type, ... }` with a small
606
+ * vocabulary: `system`, `assistant`, `user`, `result`, `rate_limit_event`.
607
+ * We branch on `type` (+ optional `subtype` for `system`) and rely on every
608
+ * other claim being optional — the renderer must not crash on a frame we've
609
+ * never seen.
610
+ */
611
+ function isClaudeStreamFrame(rec) {
612
+ return typeof rec['type'] === 'string';
613
+ }
614
+ function viewFromClaudeFrame(rec) {
615
+ const type = rec['type'];
616
+ if (type === 'system') {
617
+ const subtype = readString(rec, 'subtype');
618
+ if (subtype === 'init') {
619
+ return {
620
+ kind: 'system',
621
+ subKind: 'init',
622
+ summary: `Worker initialised — ${readString(rec, 'model') ?? 'model'}`,
623
+ raw: rec,
624
+ };
625
+ }
626
+ if (subtype === 'hook_started' || subtype === 'hook_response') {
627
+ const hookName = readString(rec, 'hook_name') ?? 'hook';
628
+ return {
629
+ kind: 'system',
630
+ subKind: 'hook',
631
+ summary: subtype === 'hook_started'
632
+ ? `Hook started — ${hookName}`
633
+ : `Hook completed — ${hookName}`,
634
+ raw: rec,
635
+ };
636
+ }
637
+ return {
638
+ kind: 'system',
639
+ subKind: 'other',
640
+ summary: `System event${subtype ? ' — ' + subtype : ''}`,
641
+ raw: rec,
642
+ };
643
+ }
644
+ if (type === 'rate_limit_event') {
645
+ const info = rec['rate_limit_info'];
646
+ let summary = 'Rate-limit event';
647
+ if (isRecord(info)) {
648
+ const status = readString(info, 'status');
649
+ const rateLimitType = readString(info, 'rateLimitType');
650
+ if (status && rateLimitType) {
651
+ summary = `Rate-limit ${status} (${rateLimitType})`;
652
+ }
653
+ }
654
+ return { kind: 'system', subKind: 'rate-limit', summary, raw: rec };
655
+ }
656
+ if (type === 'assistant') {
657
+ const message = rec['message'];
658
+ if (isRecord(message)) {
659
+ const parts = extractAssistantParts(message['content']);
660
+ const model = readString(message, 'model');
661
+ const usage = isRecord(message['usage']) ? extractUsage(message['usage']) : null;
662
+ return {
663
+ kind: 'assistant',
664
+ parts,
665
+ model,
666
+ usage,
667
+ raw: rec,
668
+ };
669
+ }
670
+ }
671
+ if (type === 'user') {
672
+ // claude wraps tool_result inside a `user` frame's content. Extract the
673
+ // first tool_result we find; if there isn't one, fall through to unknown.
674
+ const message = rec['message'];
675
+ if (isRecord(message) && Array.isArray(message['content'])) {
676
+ for (const part of message['content']) {
677
+ if (isRecord(part) && part['type'] === 'tool_result') {
678
+ const rawContent = part['content'];
679
+ return {
680
+ kind: 'tool-result',
681
+ toolUseId: readString(part, 'tool_use_id') ?? '',
682
+ content: stringifyToolContent(rawContent),
683
+ isError: readBoolean(part, 'is_error') ?? false,
684
+ raw: rec,
685
+ };
686
+ }
687
+ }
688
+ }
689
+ }
690
+ if (type === 'result') {
691
+ const usage = isRecord(rec['usage']) ? extractUsage(rec['usage']) : null;
692
+ return {
693
+ kind: 'result',
694
+ text: readString(rec, 'result') ?? '',
695
+ durationMs: readNumber(rec, 'duration_ms') ?? 0,
696
+ usage,
697
+ costUsd: readNumber(rec, 'total_cost_usd'),
698
+ reason: readString(rec, 'terminal_reason') ?? readString(rec, 'stop_reason') ?? 'completed',
699
+ isError: readBoolean(rec, 'is_error') ?? false,
700
+ raw: rec,
701
+ };
702
+ }
703
+ return {
704
+ kind: 'unknown',
705
+ summary: `Unrecognised claude frame type: ${typeof type === 'string' ? type : 'n/a'}`,
706
+ raw: rec,
707
+ };
708
+ }
709
+ function viewSystemHook(rec) {
710
+ // Hooks come through as a `Hook` event-kind on our side but the payload
711
+ // shape matches claude's hook_started / hook_response stream-json.
712
+ return viewFromClaudeFrame(rec);
713
+ }
714
+ function extractAssistantParts(content) {
715
+ if (!Array.isArray(content)) {
716
+ return [];
717
+ }
718
+ const parts = [];
719
+ for (const part of content) {
720
+ if (!isRecord(part)) {
721
+ continue;
722
+ }
723
+ if (part['type'] === 'text') {
724
+ const text = readString(part, 'text');
725
+ if (text !== null) {
726
+ parts.push({ type: 'text', text });
727
+ }
728
+ }
729
+ else if (part['type'] === 'tool_use') {
730
+ parts.push({
731
+ type: 'tool_use',
732
+ id: readString(part, 'id') ?? '',
733
+ toolName: readString(part, 'name') ?? 'tool',
734
+ input: part['input'] ?? null,
735
+ });
736
+ }
737
+ }
738
+ return parts;
739
+ }
740
+ function extractUsage(usage) {
741
+ return {
742
+ input: readNumber(usage, 'input_tokens') ?? 0,
743
+ output: readNumber(usage, 'output_tokens') ?? 0,
744
+ cacheRead: readNumber(usage, 'cache_read_input_tokens') ?? undefined,
745
+ cacheCreation: readNumber(usage, 'cache_creation_input_tokens') ?? undefined,
746
+ };
747
+ }
748
+ /**
749
+ * Tool result `content` comes in many shapes: a plain string, an array of
750
+ * `{type:'text', text:...}` blocks (the Anthropic content-block format), or
751
+ * raw JSON. Flatten everything we recognise into a single string for the
752
+ * UI; the consumer can still pop the raw payload for inspection.
753
+ */
754
+ function stringifyToolContent(content) {
755
+ if (typeof content === 'string') {
756
+ return content;
757
+ }
758
+ if (Array.isArray(content)) {
759
+ return content
760
+ .map((block) => {
761
+ if (isRecord(block) && block['type'] === 'text') {
762
+ return readString(block, 'text') ?? '';
763
+ }
764
+ return JSON.stringify(block);
765
+ })
766
+ .join('\n');
767
+ }
768
+ if (content === undefined || content === null) {
769
+ return '';
770
+ }
771
+ try {
772
+ return JSON.stringify(content, null, 2);
773
+ }
774
+ catch {
775
+ return String(content);
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Read-only transcript of a session, derived from the events stream.
781
+ *
782
+ * The component takes raw `AiSessionEventDto` entries, runs them through the
783
+ * `event-parser` to produce a typed `ParsedView` per event, and renders each
784
+ * kind with its own treatment — assistant bubbles for chat text, expandable
785
+ * cards for tool calls + tool results, single-line collapsed strips for the
786
+ * system / hook noise, and a stats panel for the terminal `result` summary.
379
787
  *
380
- * Markdown rendering is deliberately minimal in Phase 1: the component shows
381
- * the raw payload (already JSON or stream-json) in a `<pre>` block. The
382
- * concept §10 calls for proper markdown + code-block highlighting; that
383
- * upgrade lands behind a `markdown` Input once we've picked a renderer
384
- * (markdown-it vs marked) and verified its CSP behaviour.
788
+ * Markdown rendering inside assistant text is still deferred (concept §10);
789
+ * for now the parser hands plain text to the template and the SCSS preserves
790
+ * line breaks via `white-space: pre-wrap`. Picking a renderer (markdown-it vs
791
+ * marked) is gated on a CSP review and a markdown-corpus regression test.
385
792
  */
386
793
  class AiChatStreamComponent {
387
794
  events = input.required(...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
388
795
  /**
389
- * Sorted by sequence so out-of-order arrival (the SignalR + REST backfill
390
- * union) renders deterministically.
796
+ * Sorted + parsed. Recomputes whenever the input signal changes; the sort
797
+ * is stable so live appends from SignalR keep prior render positions.
391
798
  */
392
- orderedEvents = computed(() => [...this.events()].sort((a, b) => a.sequence - b.sequence), ...(ngDevMode ? [{ debugName: "orderedEvents" }] : /* istanbul ignore next */ []));
393
- variant(kind) {
394
- switch (kind) {
395
- case 'Message':
396
- return 'message';
397
- case 'ToolCall':
398
- return 'tool-call';
399
- case 'ToolResult':
400
- return 'tool-result';
401
- case 'StatusChange':
402
- return 'status';
403
- case 'Hook':
404
- return 'hook';
405
- case 'Error':
406
- return 'error';
407
- default:
408
- return 'message';
799
+ parsedEvents = computed(() => parseAndOrder(this.events()), ...(ngDevMode ? [{ debugName: "parsedEvents" }] : /* istanbul ignore next */ []));
800
+ /**
801
+ * Per-event "show raw JSON" toggle. Keyed by sequence so live appends
802
+ * don't flicker existing toggles closed. A `Set<number>` rather than a
803
+ * record so add / delete avoids `keyof` shenanigans.
804
+ */
805
+ expandedSet = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedSet" }] : /* istanbul ignore next */ []));
806
+ isExpanded(sequence) {
807
+ return this.expandedSet().has(sequence);
808
+ }
809
+ toggleRaw(sequence) {
810
+ this.expandedSet.update((prev) => {
811
+ const next = new Set(prev);
812
+ if (next.has(sequence)) {
813
+ next.delete(sequence);
814
+ }
815
+ else {
816
+ next.add(sequence);
817
+ }
818
+ return next;
819
+ });
820
+ }
821
+ /**
822
+ * `HH:MM:SS` formatter — full ISO is too noisy in a 30-event transcript.
823
+ * Returns the input unchanged if parsing fails so we don't pretend events
824
+ * we can't read are recent.
825
+ */
826
+ formatTime(iso) {
827
+ const d = new Date(iso);
828
+ if (Number.isNaN(d.getTime())) {
829
+ return iso;
830
+ }
831
+ return d.toLocaleTimeString('de-AT', { hour12: false });
832
+ }
833
+ /** Compact "1234 tok" for the assistant header. */
834
+ formatTokenCount(n) {
835
+ if (n >= 1000) {
836
+ return `${(n / 1000).toFixed(1)} ktok`;
837
+ }
838
+ return `${n} tok`;
839
+ }
840
+ /** Duration in either `ms` (sub-second) or `s.SS`. */
841
+ formatDuration(ms) {
842
+ if (ms < 1000) {
843
+ return `${Math.round(ms)} ms`;
844
+ }
845
+ return `${(ms / 1000).toFixed(2)} s`;
846
+ }
847
+ /** Cost in EUR-style decimals; null + zero collapse to a dash. */
848
+ formatCost(usd) {
849
+ if (usd == null || usd === 0) {
850
+ return '—';
851
+ }
852
+ return `$${usd.toFixed(4)}`;
853
+ }
854
+ /** Pretty-print arbitrary tool_use input or raw blocks for the expandable detail panel. */
855
+ formatRaw(value) {
856
+ if (value === undefined || value === null) {
857
+ return '';
858
+ }
859
+ if (typeof value === 'string') {
860
+ return value;
861
+ }
862
+ try {
863
+ return JSON.stringify(value, null, 2);
864
+ }
865
+ catch {
866
+ return String(value);
409
867
  }
410
868
  }
411
869
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiChatStreamComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
412
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiChatStreamComponent, isStandalone: true, selector: "mm-ai-chat-stream", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"mm-ai-chat\">\n @for (event of orderedEvents(); track event.sequence) {\n <article class=\"mm-ai-chat__event mm-ai-chat__event--{{ variant(event.kind) }}\">\n <header class=\"mm-ai-chat__event-header\">\n <span class=\"mm-ai-chat__kind\">{{ event.kind }}</span>\n <span class=\"mm-ai-chat__actor\">{{ event.actorRef }}</span>\n <span class=\"mm-ai-chat__sequence\">#{{ event.sequence }}</span>\n <time class=\"mm-ai-chat__time\" [attr.datetime]=\"event.at\">\n {{ event.at }}\n </time>\n </header>\n <pre class=\"mm-ai-chat__payload\">{{ event.payload }}</pre>\n </article>\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: #16191d;--mm-ai-chat-card-bg: #1f2227;--mm-ai-chat-border: #2f343b;--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9aa0a6;--mm-ai-chat-message: #1f6feb;--mm-ai-chat-tool-call: #d29922;--mm-ai-chat-tool-result: #2ea44f;--mm-ai-chat-status: #6e7681;--mm-ai-chat-hook: #8957e5;--mm-ai-chat-error: #d73a49}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);border-radius:.5rem}.mm-ai-chat__event{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left-width:3px;border-radius:.4rem;padding:.5rem .6rem}.mm-ai-chat__event--message{border-left-color:var(--mm-ai-chat-message)}.mm-ai-chat__event--tool-call{border-left-color:var(--mm-ai-chat-tool-call)}.mm-ai-chat__event--tool-result{border-left-color:var(--mm-ai-chat-tool-result)}.mm-ai-chat__event--status{border-left-color:var(--mm-ai-chat-status)}.mm-ai-chat__event--hook{border-left-color:var(--mm-ai-chat-hook)}.mm-ai-chat__event--error{border-left-color:var(--mm-ai-chat-error)}.mm-ai-chat__event-header{display:flex;gap:.5rem;align-items:baseline;font-size:.75rem;color:var(--mm-ai-chat-muted);margin-bottom:.3rem}.mm-ai-chat__kind{font-weight:700;color:var(--mm-ai-chat-text);text-transform:uppercase;letter-spacing:.04em}.mm-ai-chat__sequence,.mm-ai-chat__time{font-variant-numeric:tabular-nums}.mm-ai-chat__time{margin-left:auto}.mm-ai-chat__payload{margin:0;font-size:.8rem;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1rem;font-style:italic}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
870
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiChatStreamComponent, isStandalone: true, selector: "mm-ai-chat-stream", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"mm-ai-chat\">\n @for (parsed of parsedEvents(); track parsed.event.sequence) {\n @switch (parsed.view.kind) {\n @case ('assistant') {\n <article class=\"mm-ai-chat__bubble mm-ai-chat__bubble--assistant\">\n <header class=\"mm-ai-chat__bubble-header\">\n <span class=\"mm-ai-chat__role\">Assistant</span>\n @if (parsed.view.model; as model) {\n <span class=\"mm-ai-chat__meta\">{{ model }}</span>\n }\n @if (parsed.view.usage; as usage) {\n <span class=\"mm-ai-chat__meta\">\n {{ formatTokenCount(usage.input + usage.output) }}\n </span>\n }\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n @for (part of parsed.view.parts; track $index) {\n @switch (part.type) {\n @case ('text') {\n <p class=\"mm-ai-chat__text\">{{ part.text }}</p>\n }\n @case ('tool_use') {\n <div class=\"mm-ai-chat__tool-use\">\n <header class=\"mm-ai-chat__tool-use-header\">\n <span class=\"mm-ai-chat__tool-badge\">tool</span>\n <span class=\"mm-ai-chat__tool-name\">{{ part.toolName }}</span>\n </header>\n <pre class=\"mm-ai-chat__tool-input\">{{ formatRaw(part.input) }}</pre>\n </div>\n }\n }\n }\n </article>\n }\n\n @case ('tool-result') {\n <article\n class=\"mm-ai-chat__tool-result\"\n [class.mm-ai-chat__tool-result--error]=\"parsed.view.isError\"\n >\n <header class=\"mm-ai-chat__tool-result-header\">\n <span class=\"mm-ai-chat__role\">Tool result</span>\n @if (parsed.view.isError) {\n <span class=\"mm-ai-chat__error-badge\">error</span>\n }\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n <pre class=\"mm-ai-chat__tool-result-body\">{{ parsed.view.content }}</pre>\n </article>\n }\n\n @case ('result') {\n <article class=\"mm-ai-chat__result\"\n [class.mm-ai-chat__result--error]=\"parsed.view.isError\">\n <header class=\"mm-ai-chat__result-header\">\n <span class=\"mm-ai-chat__role\">{{ parsed.view.reason }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n @if (parsed.view.text) {\n <p class=\"mm-ai-chat__result-text\">{{ parsed.view.text }}</p>\n }\n <dl class=\"mm-ai-chat__result-stats\">\n <div>\n <dt>Duration</dt>\n <dd>{{ formatDuration(parsed.view.durationMs) }}</dd>\n </div>\n <div>\n <dt>Tokens (in/out)</dt>\n <dd>\n @if (parsed.view.usage) {\n {{ formatTokenCount(parsed.view.usage.input) }}\n /\n {{ formatTokenCount(parsed.view.usage.output) }}\n } @else {\n \u2014\n }\n </dd>\n </div>\n <div>\n <dt>Cost</dt>\n <dd>{{ formatCost(parsed.view.costUsd) }}</dd>\n </div>\n </dl>\n </article>\n }\n\n @case ('system') {\n <button\n type=\"button\"\n class=\"mm-ai-chat__system\"\n [class.mm-ai-chat__system--init]=\"parsed.view.subKind === 'init'\"\n [class.mm-ai-chat__system--hook]=\"parsed.view.subKind === 'hook'\"\n [class.mm-ai-chat__system--rate]=\"parsed.view.subKind === 'rate-limit'\"\n [attr.aria-expanded]=\"isExpanded(parsed.event.sequence)\"\n (click)=\"toggleRaw(parsed.event.sequence)\"\n >\n <span class=\"mm-ai-chat__system-chevron\">\n {{ isExpanded(parsed.event.sequence) ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-chat__system-summary\">{{ parsed.view.summary }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </button>\n @if (isExpanded(parsed.event.sequence)) {\n <pre class=\"mm-ai-chat__system-detail\">{{ formatRaw(parsed.view.raw) }}</pre>\n }\n }\n\n @case ('status-change') {\n <div class=\"mm-ai-chat__status\">\n <span class=\"mm-ai-chat__status-label\">Status</span>\n <span class=\"mm-ai-chat__status-value\">{{ parsed.view.status }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </div>\n }\n\n @case ('unknown') {\n <button\n type=\"button\"\n class=\"mm-ai-chat__system mm-ai-chat__system--unknown\"\n [attr.aria-expanded]=\"isExpanded(parsed.event.sequence)\"\n (click)=\"toggleRaw(parsed.event.sequence)\"\n >\n <span class=\"mm-ai-chat__system-chevron\">\n {{ isExpanded(parsed.event.sequence) ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-chat__system-summary\">{{ parsed.view.summary }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </button>\n @if (isExpanded(parsed.event.sequence)) {\n <pre class=\"mm-ai-chat__system-detail\">{{ formatRaw(parsed.view.raw) }}</pre>\n }\n }\n }\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: transparent;--mm-ai-chat-card-bg: rgba(31, 46, 64, .55);--mm-ai-chat-card-bg-elevated: rgba(31, 46, 64, .85);--mm-ai-chat-border: rgba(100, 206, 185, .18);--mm-ai-chat-border-strong: rgba(100, 206, 185, .4);--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9292a6;--mm-ai-chat-accent-assistant: #64ceb9;--mm-ai-chat-accent-tool: #00a8dc;--mm-ai-chat-accent-tool-result: #00a8dc;--mm-ai-chat-accent-system: #6c4da8;--mm-ai-chat-accent-rate-limit: #da9162;--mm-ai-chat-accent-result: #64ceb9;--mm-ai-chat-accent-error: #ec658f}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.25rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);font-size:.85rem;line-height:1.45}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1.5rem;font-style:italic}.mm-ai-chat__bubble{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left:3px solid var(--mm-ai-chat-accent-assistant);border-radius:.5rem;padding:.6rem .75rem;display:flex;flex-direction:column;gap:.5rem}.mm-ai-chat__bubble-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__role{font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--mm-ai-chat-accent-assistant)}.mm-ai-chat__meta{font-family:Roboto Mono,monospace;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__time{margin-left:auto;font-variant-numeric:tabular-nums;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__text{margin:0;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__tool-use{border:1px solid var(--mm-ai-chat-border);border-left:2px solid var(--mm-ai-chat-accent-tool);border-radius:.35rem;background:var(--mm-ai-chat-card-bg-elevated);padding:.4rem .5rem}.mm-ai-chat__tool-use-header{display:flex;align-items:baseline;gap:.4rem;font-size:.7rem}.mm-ai-chat__tool-badge{background:var(--mm-ai-chat-accent-tool);color:#07172b;padding:0 6px;border-radius:.25rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;font-size:.65rem}.mm-ai-chat__tool-name{font-family:Roboto Mono,monospace;color:var(--mm-ai-chat-text);font-weight:600}.mm-ai-chat__tool-input{margin:.35rem 0 0;font-family:Roboto Mono,monospace;font-size:.75rem;white-space:pre-wrap;word-break:break-word;color:var(--mm-ai-chat-muted);max-height:14rem;overflow-y:auto}.mm-ai-chat__tool-result{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left:3px solid var(--mm-ai-chat-accent-tool-result);border-radius:.5rem;padding:.55rem .75rem;margin-left:1.25rem}.mm-ai-chat__tool-result--error{border-left-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__tool-result-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__tool-result-header .mm-ai-chat__role{color:var(--mm-ai-chat-accent-tool-result)}.mm-ai-chat__error-badge{background:var(--mm-ai-chat-accent-error);color:#07172b;padding:0 6px;border-radius:.25rem;font-size:.6rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.mm-ai-chat__tool-result-body{margin:.4rem 0 0;font-family:Roboto Mono,monospace;font-size:.75rem;white-space:pre-wrap;word-break:break-word;max-height:18rem;overflow-y:auto}.mm-ai-chat__result{background:var(--mm-ai-chat-card-bg-elevated);border:1px solid var(--mm-ai-chat-border-strong);border-top:3px solid var(--mm-ai-chat-accent-result);border-radius:.5rem;padding:.7rem .85rem;display:flex;flex-direction:column;gap:.5rem}.mm-ai-chat__result--error{border-top-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__result-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__result-header .mm-ai-chat__role{color:var(--mm-ai-chat-accent-result);text-transform:uppercase}.mm-ai-chat__result-text{margin:0;white-space:pre-wrap;word-break:break-word;font-style:italic;color:var(--mm-ai-chat-text)}.mm-ai-chat__result-stats{margin:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(7rem,1fr));gap:.5rem;font-family:Roboto Mono,monospace;font-size:.75rem}.mm-ai-chat__result-stats div{display:flex;flex-direction:column;gap:.2rem;padding:.4rem .5rem;background:#64ceb90f;border-radius:.3rem}.mm-ai-chat__result-stats dt{font-size:.62rem;text-transform:uppercase;letter-spacing:.08em;color:var(--mm-ai-chat-muted)}.mm-ai-chat__result-stats dd{margin:0;color:var(--mm-ai-chat-text);font-variant-numeric:tabular-nums}.mm-ai-chat__system{all:unset;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.35rem .6rem;background:#64ceb908;border:1px solid var(--mm-ai-chat-border);border-left:2px solid var(--mm-ai-chat-accent-system);border-radius:.4rem;font-size:.78rem;color:var(--mm-ai-chat-muted);transition:background-color .15s ease}.mm-ai-chat__system:hover{background:#64ceb90f}.mm-ai-chat__system:focus-visible{outline:2px solid var(--mm-ai-chat-accent-assistant);outline-offset:1px}.mm-ai-chat__system--init{border-left-color:var(--mm-ai-chat-accent-tool)}.mm-ai-chat__system--hook{border-left-color:var(--mm-ai-chat-accent-system)}.mm-ai-chat__system--rate{border-left-color:var(--mm-ai-chat-accent-rate-limit)}.mm-ai-chat__system--unknown{border-left-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__system-chevron{font-size:.7rem;width:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__system-summary{flex:1;color:var(--mm-ai-chat-text);font-variant-numeric:tabular-nums}.mm-ai-chat__system-detail{margin:-.25rem 0 0 1.5rem;padding:.5rem .6rem;background:#00000040;border-radius:.35rem;font-family:Roboto Mono,monospace;font-size:.7rem;white-space:pre-wrap;word-break:break-word;max-height:22rem;overflow-y:auto;color:var(--mm-ai-chat-muted)}.mm-ai-chat__status{display:flex;align-items:baseline;gap:.5rem;padding:.25rem .5rem;font-size:.7rem;color:var(--mm-ai-chat-muted);text-transform:uppercase;letter-spacing:.06em}.mm-ai-chat__status-label{color:var(--mm-ai-chat-muted)}.mm-ai-chat__status-value{color:var(--mm-ai-chat-text);font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
413
871
  }
414
872
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiChatStreamComponent, decorators: [{
415
873
  type: Component,
416
- args: [{ selector: 'mm-ai-chat-stream', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-chat\">\n @for (event of orderedEvents(); track event.sequence) {\n <article class=\"mm-ai-chat__event mm-ai-chat__event--{{ variant(event.kind) }}\">\n <header class=\"mm-ai-chat__event-header\">\n <span class=\"mm-ai-chat__kind\">{{ event.kind }}</span>\n <span class=\"mm-ai-chat__actor\">{{ event.actorRef }}</span>\n <span class=\"mm-ai-chat__sequence\">#{{ event.sequence }}</span>\n <time class=\"mm-ai-chat__time\" [attr.datetime]=\"event.at\">\n {{ event.at }}\n </time>\n </header>\n <pre class=\"mm-ai-chat__payload\">{{ event.payload }}</pre>\n </article>\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: #16191d;--mm-ai-chat-card-bg: #1f2227;--mm-ai-chat-border: #2f343b;--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9aa0a6;--mm-ai-chat-message: #1f6feb;--mm-ai-chat-tool-call: #d29922;--mm-ai-chat-tool-result: #2ea44f;--mm-ai-chat-status: #6e7681;--mm-ai-chat-hook: #8957e5;--mm-ai-chat-error: #d73a49}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);border-radius:.5rem}.mm-ai-chat__event{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left-width:3px;border-radius:.4rem;padding:.5rem .6rem}.mm-ai-chat__event--message{border-left-color:var(--mm-ai-chat-message)}.mm-ai-chat__event--tool-call{border-left-color:var(--mm-ai-chat-tool-call)}.mm-ai-chat__event--tool-result{border-left-color:var(--mm-ai-chat-tool-result)}.mm-ai-chat__event--status{border-left-color:var(--mm-ai-chat-status)}.mm-ai-chat__event--hook{border-left-color:var(--mm-ai-chat-hook)}.mm-ai-chat__event--error{border-left-color:var(--mm-ai-chat-error)}.mm-ai-chat__event-header{display:flex;gap:.5rem;align-items:baseline;font-size:.75rem;color:var(--mm-ai-chat-muted);margin-bottom:.3rem}.mm-ai-chat__kind{font-weight:700;color:var(--mm-ai-chat-text);text-transform:uppercase;letter-spacing:.04em}.mm-ai-chat__sequence,.mm-ai-chat__time{font-variant-numeric:tabular-nums}.mm-ai-chat__time{margin-left:auto}.mm-ai-chat__payload{margin:0;font-size:.8rem;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1rem;font-style:italic}\n"] }]
874
+ args: [{ selector: 'mm-ai-chat-stream', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-chat\">\n @for (parsed of parsedEvents(); track parsed.event.sequence) {\n @switch (parsed.view.kind) {\n @case ('assistant') {\n <article class=\"mm-ai-chat__bubble mm-ai-chat__bubble--assistant\">\n <header class=\"mm-ai-chat__bubble-header\">\n <span class=\"mm-ai-chat__role\">Assistant</span>\n @if (parsed.view.model; as model) {\n <span class=\"mm-ai-chat__meta\">{{ model }}</span>\n }\n @if (parsed.view.usage; as usage) {\n <span class=\"mm-ai-chat__meta\">\n {{ formatTokenCount(usage.input + usage.output) }}\n </span>\n }\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n @for (part of parsed.view.parts; track $index) {\n @switch (part.type) {\n @case ('text') {\n <p class=\"mm-ai-chat__text\">{{ part.text }}</p>\n }\n @case ('tool_use') {\n <div class=\"mm-ai-chat__tool-use\">\n <header class=\"mm-ai-chat__tool-use-header\">\n <span class=\"mm-ai-chat__tool-badge\">tool</span>\n <span class=\"mm-ai-chat__tool-name\">{{ part.toolName }}</span>\n </header>\n <pre class=\"mm-ai-chat__tool-input\">{{ formatRaw(part.input) }}</pre>\n </div>\n }\n }\n }\n </article>\n }\n\n @case ('tool-result') {\n <article\n class=\"mm-ai-chat__tool-result\"\n [class.mm-ai-chat__tool-result--error]=\"parsed.view.isError\"\n >\n <header class=\"mm-ai-chat__tool-result-header\">\n <span class=\"mm-ai-chat__role\">Tool result</span>\n @if (parsed.view.isError) {\n <span class=\"mm-ai-chat__error-badge\">error</span>\n }\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n <pre class=\"mm-ai-chat__tool-result-body\">{{ parsed.view.content }}</pre>\n </article>\n }\n\n @case ('result') {\n <article class=\"mm-ai-chat__result\"\n [class.mm-ai-chat__result--error]=\"parsed.view.isError\">\n <header class=\"mm-ai-chat__result-header\">\n <span class=\"mm-ai-chat__role\">{{ parsed.view.reason }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </header>\n @if (parsed.view.text) {\n <p class=\"mm-ai-chat__result-text\">{{ parsed.view.text }}</p>\n }\n <dl class=\"mm-ai-chat__result-stats\">\n <div>\n <dt>Duration</dt>\n <dd>{{ formatDuration(parsed.view.durationMs) }}</dd>\n </div>\n <div>\n <dt>Tokens (in/out)</dt>\n <dd>\n @if (parsed.view.usage) {\n {{ formatTokenCount(parsed.view.usage.input) }}\n /\n {{ formatTokenCount(parsed.view.usage.output) }}\n } @else {\n \u2014\n }\n </dd>\n </div>\n <div>\n <dt>Cost</dt>\n <dd>{{ formatCost(parsed.view.costUsd) }}</dd>\n </div>\n </dl>\n </article>\n }\n\n @case ('system') {\n <button\n type=\"button\"\n class=\"mm-ai-chat__system\"\n [class.mm-ai-chat__system--init]=\"parsed.view.subKind === 'init'\"\n [class.mm-ai-chat__system--hook]=\"parsed.view.subKind === 'hook'\"\n [class.mm-ai-chat__system--rate]=\"parsed.view.subKind === 'rate-limit'\"\n [attr.aria-expanded]=\"isExpanded(parsed.event.sequence)\"\n (click)=\"toggleRaw(parsed.event.sequence)\"\n >\n <span class=\"mm-ai-chat__system-chevron\">\n {{ isExpanded(parsed.event.sequence) ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-chat__system-summary\">{{ parsed.view.summary }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </button>\n @if (isExpanded(parsed.event.sequence)) {\n <pre class=\"mm-ai-chat__system-detail\">{{ formatRaw(parsed.view.raw) }}</pre>\n }\n }\n\n @case ('status-change') {\n <div class=\"mm-ai-chat__status\">\n <span class=\"mm-ai-chat__status-label\">Status</span>\n <span class=\"mm-ai-chat__status-value\">{{ parsed.view.status }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </div>\n }\n\n @case ('unknown') {\n <button\n type=\"button\"\n class=\"mm-ai-chat__system mm-ai-chat__system--unknown\"\n [attr.aria-expanded]=\"isExpanded(parsed.event.sequence)\"\n (click)=\"toggleRaw(parsed.event.sequence)\"\n >\n <span class=\"mm-ai-chat__system-chevron\">\n {{ isExpanded(parsed.event.sequence) ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-chat__system-summary\">{{ parsed.view.summary }}</span>\n <time class=\"mm-ai-chat__time\">{{ formatTime(parsed.event.at) }}</time>\n </button>\n @if (isExpanded(parsed.event.sequence)) {\n <pre class=\"mm-ai-chat__system-detail\">{{ formatRaw(parsed.view.raw) }}</pre>\n }\n }\n }\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: transparent;--mm-ai-chat-card-bg: rgba(31, 46, 64, .55);--mm-ai-chat-card-bg-elevated: rgba(31, 46, 64, .85);--mm-ai-chat-border: rgba(100, 206, 185, .18);--mm-ai-chat-border-strong: rgba(100, 206, 185, .4);--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9292a6;--mm-ai-chat-accent-assistant: #64ceb9;--mm-ai-chat-accent-tool: #00a8dc;--mm-ai-chat-accent-tool-result: #00a8dc;--mm-ai-chat-accent-system: #6c4da8;--mm-ai-chat-accent-rate-limit: #da9162;--mm-ai-chat-accent-result: #64ceb9;--mm-ai-chat-accent-error: #ec658f}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.25rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);font-size:.85rem;line-height:1.45}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1.5rem;font-style:italic}.mm-ai-chat__bubble{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left:3px solid var(--mm-ai-chat-accent-assistant);border-radius:.5rem;padding:.6rem .75rem;display:flex;flex-direction:column;gap:.5rem}.mm-ai-chat__bubble-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__role{font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--mm-ai-chat-accent-assistant)}.mm-ai-chat__meta{font-family:Roboto Mono,monospace;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__time{margin-left:auto;font-variant-numeric:tabular-nums;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__text{margin:0;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__tool-use{border:1px solid var(--mm-ai-chat-border);border-left:2px solid var(--mm-ai-chat-accent-tool);border-radius:.35rem;background:var(--mm-ai-chat-card-bg-elevated);padding:.4rem .5rem}.mm-ai-chat__tool-use-header{display:flex;align-items:baseline;gap:.4rem;font-size:.7rem}.mm-ai-chat__tool-badge{background:var(--mm-ai-chat-accent-tool);color:#07172b;padding:0 6px;border-radius:.25rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;font-size:.65rem}.mm-ai-chat__tool-name{font-family:Roboto Mono,monospace;color:var(--mm-ai-chat-text);font-weight:600}.mm-ai-chat__tool-input{margin:.35rem 0 0;font-family:Roboto Mono,monospace;font-size:.75rem;white-space:pre-wrap;word-break:break-word;color:var(--mm-ai-chat-muted);max-height:14rem;overflow-y:auto}.mm-ai-chat__tool-result{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left:3px solid var(--mm-ai-chat-accent-tool-result);border-radius:.5rem;padding:.55rem .75rem;margin-left:1.25rem}.mm-ai-chat__tool-result--error{border-left-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__tool-result-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__tool-result-header .mm-ai-chat__role{color:var(--mm-ai-chat-accent-tool-result)}.mm-ai-chat__error-badge{background:var(--mm-ai-chat-accent-error);color:#07172b;padding:0 6px;border-radius:.25rem;font-size:.6rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.mm-ai-chat__tool-result-body{margin:.4rem 0 0;font-family:Roboto Mono,monospace;font-size:.75rem;white-space:pre-wrap;word-break:break-word;max-height:18rem;overflow-y:auto}.mm-ai-chat__result{background:var(--mm-ai-chat-card-bg-elevated);border:1px solid var(--mm-ai-chat-border-strong);border-top:3px solid var(--mm-ai-chat-accent-result);border-radius:.5rem;padding:.7rem .85rem;display:flex;flex-direction:column;gap:.5rem}.mm-ai-chat__result--error{border-top-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__result-header{display:flex;align-items:baseline;gap:.5rem;font-size:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__result-header .mm-ai-chat__role{color:var(--mm-ai-chat-accent-result);text-transform:uppercase}.mm-ai-chat__result-text{margin:0;white-space:pre-wrap;word-break:break-word;font-style:italic;color:var(--mm-ai-chat-text)}.mm-ai-chat__result-stats{margin:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(7rem,1fr));gap:.5rem;font-family:Roboto Mono,monospace;font-size:.75rem}.mm-ai-chat__result-stats div{display:flex;flex-direction:column;gap:.2rem;padding:.4rem .5rem;background:#64ceb90f;border-radius:.3rem}.mm-ai-chat__result-stats dt{font-size:.62rem;text-transform:uppercase;letter-spacing:.08em;color:var(--mm-ai-chat-muted)}.mm-ai-chat__result-stats dd{margin:0;color:var(--mm-ai-chat-text);font-variant-numeric:tabular-nums}.mm-ai-chat__system{all:unset;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.35rem .6rem;background:#64ceb908;border:1px solid var(--mm-ai-chat-border);border-left:2px solid var(--mm-ai-chat-accent-system);border-radius:.4rem;font-size:.78rem;color:var(--mm-ai-chat-muted);transition:background-color .15s ease}.mm-ai-chat__system:hover{background:#64ceb90f}.mm-ai-chat__system:focus-visible{outline:2px solid var(--mm-ai-chat-accent-assistant);outline-offset:1px}.mm-ai-chat__system--init{border-left-color:var(--mm-ai-chat-accent-tool)}.mm-ai-chat__system--hook{border-left-color:var(--mm-ai-chat-accent-system)}.mm-ai-chat__system--rate{border-left-color:var(--mm-ai-chat-accent-rate-limit)}.mm-ai-chat__system--unknown{border-left-color:var(--mm-ai-chat-accent-error)}.mm-ai-chat__system-chevron{font-size:.7rem;width:.7rem;color:var(--mm-ai-chat-muted)}.mm-ai-chat__system-summary{flex:1;color:var(--mm-ai-chat-text);font-variant-numeric:tabular-nums}.mm-ai-chat__system-detail{margin:-.25rem 0 0 1.5rem;padding:.5rem .6rem;background:#00000040;border-radius:.35rem;font-family:Roboto Mono,monospace;font-size:.7rem;white-space:pre-wrap;word-break:break-word;max-height:22rem;overflow-y:auto;color:var(--mm-ai-chat-muted)}.mm-ai-chat__status{display:flex;align-items:baseline;gap:.5rem;padding:.25rem .5rem;font-size:.7rem;color:var(--mm-ai-chat-muted);text-transform:uppercase;letter-spacing:.06em}.mm-ai-chat__status-label{color:var(--mm-ai-chat-muted)}.mm-ai-chat__status-value{color:var(--mm-ai-chat-text);font-weight:600}\n"] }]
417
875
  }], propDecorators: { events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: true }] }] } });
418
876
 
419
877
  /**
@@ -674,6 +1132,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
674
1132
  args: [{ selector: 'mm-ai-credential-ticket-issue', imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"mm-ai-ticket\">\n <header class=\"mm-ai-ticket__header\">\n <div class=\"mm-ai-ticket__title\">Issue credential ticket</div>\n <div class=\"mm-ai-ticket__subtitle\">\n Mint a one-time code an operator redeems on their own machine.\n </div>\n </header>\n\n @if (!issued()) {\n <form\n class=\"mm-ai-ticket__form\"\n (ngSubmit)=\"onIssue()\"\n #issueForm=\"ngForm\"\n >\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">Scope</span>\n <select\n class=\"mm-ai-ticket__select\"\n [ngModel]=\"scope()\"\n (ngModelChange)=\"scope.set($event)\"\n name=\"scope\"\n [disabled]=\"issuing()\"\n >\n @for (s of scopes; track s.value) {\n <option [value]=\"s.value\">{{ s.label }}</option>\n }\n </select>\n </label>\n <p class=\"mm-ai-ticket__hint\">\n @for (s of scopes; track s.value) {\n @if (s.value === scope()) {\n {{ s.hint }}\n }\n }\n </p>\n\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">TTL (minutes)</span>\n <input\n class=\"mm-ai-ticket__input\"\n type=\"number\"\n min=\"1\"\n max=\"60\"\n step=\"1\"\n [ngModel]=\"ttlMinutes()\"\n (ngModelChange)=\"ttlMinutes.set(+$event)\"\n name=\"ttlMinutes\"\n [disabled]=\"issuing()\"\n />\n </label>\n <p class=\"mm-ai-ticket__hint\">\n Default 5 minutes. Server clamps to 60 \u2014 long TTLs leak.\n </p>\n\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--primary\"\n type=\"submit\"\n [disabled]=\"issuing()\"\n >\n @if (issuing()) {\n Issuing\u2026\n } @else {\n Issue ticket\n }\n </button>\n </div>\n\n @if (error()) {\n <p class=\"mm-ai-ticket__error\">{{ error() }}</p>\n }\n </form>\n } @else {\n <div class=\"mm-ai-ticket__result\">\n <p class=\"mm-ai-ticket__result-label\">\n One-time code (copy now \u2014 not shown again):\n </p>\n <div class=\"mm-ai-ticket__code-row\">\n <code class=\"mm-ai-ticket__code\">{{ issued()!.code }}</code>\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--secondary\"\n type=\"button\"\n (click)=\"onCopy()\"\n >\n @if (copied()) {\n Copied\n } @else {\n Copy\n }\n </button>\n </div>\n <dl class=\"mm-ai-ticket__meta\">\n <div>\n <dt>Scope</dt>\n <dd>{{ issued()!.scope }}</dd>\n </div>\n <div>\n <dt>Expires at</dt>\n <dd>{{ issued()!.expiresAt }}</dd>\n </div>\n <div>\n <dt>Ticket rtId</dt>\n <dd><code>{{ issued()!.rtId }}</code></dd>\n </div>\n </dl>\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button\"\n type=\"button\"\n (click)=\"onClear()\"\n >\n Issue another\n </button>\n </div>\n </div>\n }\n</section>\n", styles: [":host{display:block;--mm-ai-ticket-bg: #16191d;--mm-ai-ticket-surface: #1f2227;--mm-ai-ticket-border: #2f343b;--mm-ai-ticket-text: #e6e6e6;--mm-ai-ticket-muted: #9aa0a6;--mm-ai-ticket-accent: #64ceb9;--mm-ai-ticket-error: #ec658f;--mm-ai-ticket-code-bg: #0f1115}.mm-ai-ticket{background:var(--mm-ai-ticket-bg);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.5rem;padding:1rem 1.25rem;display:flex;flex-direction:column;gap:.85rem;font-family:inherit}.mm-ai-ticket__header{display:flex;flex-direction:column;gap:.15rem}.mm-ai-ticket__title{font-size:1rem;font-weight:600}.mm-ai-ticket__subtitle{font-size:.8rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__form,.mm-ai-ticket__result{display:flex;flex-direction:column;gap:.6rem}.mm-ai-ticket__field{display:flex;flex-direction:column;gap:.25rem}.mm-ai-ticket__label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__select,.mm-ai-ticket__input{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .55rem;font-family:inherit;font-size:.9rem}.mm-ai-ticket__select:focus,.mm-ai-ticket__input:focus{outline:2px solid var(--mm-ai-ticket-accent);outline-offset:1px}.mm-ai-ticket__select:disabled,.mm-ai-ticket__input:disabled{opacity:.6;cursor:not-allowed}.mm-ai-ticket__hint{margin:0;font-size:.75rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__actions{display:flex;gap:.5rem;margin-top:.25rem}.mm-ai-ticket__button{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .85rem;font-family:inherit;font-size:.85rem;cursor:pointer}.mm-ai-ticket__button:hover:not(:disabled){border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__button:disabled{opacity:.5;cursor:not-allowed}.mm-ai-ticket__button--primary{background:var(--mm-ai-ticket-accent);color:#0a0c0f;border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__error{margin:0;font-size:.8rem;color:var(--mm-ai-ticket-error)}.mm-ai-ticket__result-label{margin:0;font-size:.85rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__code-row{display:flex;align-items:center;gap:.5rem}.mm-ai-ticket__code{flex:1;background:var(--mm-ai-ticket-code-bg);color:var(--mm-ai-ticket-accent);padding:.5rem .75rem;border-radius:.3rem;font-family:Roboto Mono,ui-monospace,monospace;font-size:1.05rem;letter-spacing:.1em;-webkit-user-select:all;user-select:all}.mm-ai-ticket__meta{display:grid;grid-template-columns:max-content 1fr;gap:.2rem .75rem;margin:0;font-size:.8rem}.mm-ai-ticket__meta>div{display:contents}.mm-ai-ticket__meta dt{color:var(--mm-ai-ticket-muted);text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.mm-ai-ticket__meta dd{margin:0;font-family:Roboto Mono,ui-monospace,monospace}\n"] }]
675
1133
  }] });
676
1134
 
1135
+ /**
1136
+ * "New App from Template" wizard (#4145). Presentation-only — the host fetches
1137
+ * the template catalogue via {@link AiAdapterClientService.getAppTemplates},
1138
+ * feeds it into the `templates` input, and listens for `submit` / `cancel` to
1139
+ * call `createSession`.
1140
+ *
1141
+ * The component is intentionally framework-agnostic (no Kendo, no CDK overlay)
1142
+ * so consumers can wrap it in whatever modal shell they already have — same
1143
+ * convention as `AiApprovalModalComponent`.
1144
+ *
1145
+ * The interpolation engine is *deliberately* dumb: a regex substitution over
1146
+ * `{{placeholder}}` tokens. The wizard only collects two inputs (project name +
1147
+ * primary entity), and a full Handlebars dependency for two tokens would be
1148
+ * overkill. If a future template needs richer rendering, switch this to
1149
+ * `handlebars.precompile`-at-build-time and re-evaluate.
1150
+ */
1151
+ class AiNewAppDialogComponent {
1152
+ /** Available templates from the adapter — typically 3 in Phase 1. */
1153
+ templates = input.required(...(ngDevMode ? [{ debugName: "templates" }] : /* istanbul ignore next */ []));
1154
+ /** Tenant name shown in the dialog header so the operator confirms the scope. */
1155
+ tenantName = input('', ...(ngDevMode ? [{ debugName: "tenantName" }] : /* istanbul ignore next */ []));
1156
+ /**
1157
+ * Emitted on Submit with the fully interpolated Goal + the picked template id.
1158
+ * Named `confirmed` (not `submit`) because Angular flags native-event collisions
1159
+ * — `submit` is a DOM event and triggers `@angular-eslint/no-output-native`.
1160
+ */
1161
+ confirmed = output();
1162
+ /**
1163
+ * Emitted on Cancel / dismiss. The host hides the wizard on this signal.
1164
+ * Named `cancelled` for the same reason `confirmed` is not `submit`.
1165
+ */
1166
+ cancelled = output();
1167
+ /** rtId-style id of the picked template, or empty until the operator picks one. */
1168
+ selectedTemplateId = signal('', ...(ngDevMode ? [{ debugName: "selectedTemplateId" }] : /* istanbul ignore next */ []));
1169
+ projectName = signal('', ...(ngDevMode ? [{ debugName: "projectName" }] : /* istanbul ignore next */ []));
1170
+ primaryEntity = signal('', ...(ngDevMode ? [{ debugName: "primaryEntity" }] : /* istanbul ignore next */ []));
1171
+ selectedTemplate = computed(() => {
1172
+ const id = this.selectedTemplateId();
1173
+ return this.templates().find((t) => t.id === id) ?? null;
1174
+ }, ...(ngDevMode ? [{ debugName: "selectedTemplate" }] : /* istanbul ignore next */ []));
1175
+ /**
1176
+ * Rendered Goal string the operator sees in the preview block. Stays empty
1177
+ * until a template is picked AND every required placeholder has a value, so
1178
+ * the operator never sees a half-interpolated string.
1179
+ */
1180
+ renderedGoal = computed(() => {
1181
+ const template = this.selectedTemplate();
1182
+ if (!template) {
1183
+ return '';
1184
+ }
1185
+ if (!this.allRequiredFilled()) {
1186
+ return '';
1187
+ }
1188
+ return this.interpolate(template.goalTemplate);
1189
+ }, ...(ngDevMode ? [{ debugName: "renderedGoal" }] : /* istanbul ignore next */ []));
1190
+ /** True when every placeholder the picked template declares has a non-empty value. */
1191
+ allRequiredFilled = computed(() => {
1192
+ const template = this.selectedTemplate();
1193
+ if (!template) {
1194
+ return false;
1195
+ }
1196
+ return template.requiredPlaceholders.every((key) => {
1197
+ const value = this.valueFor(key);
1198
+ return value !== null && value.trim().length > 0;
1199
+ });
1200
+ }, ...(ngDevMode ? [{ debugName: "allRequiredFilled" }] : /* istanbul ignore next */ []));
1201
+ onSubmit() {
1202
+ const template = this.selectedTemplate();
1203
+ if (!template || !this.allRequiredFilled()) {
1204
+ return;
1205
+ }
1206
+ this.confirmed.emit({
1207
+ templateId: template.id,
1208
+ jobKind: template.jobKind,
1209
+ projectName: this.projectName().trim(),
1210
+ primaryEntity: this.primaryEntity().trim(),
1211
+ goal: this.interpolate(template.goalTemplate),
1212
+ });
1213
+ }
1214
+ onCancel() {
1215
+ this.cancelled.emit();
1216
+ }
1217
+ /** Read the signal that backs the named placeholder, or `null` if unknown. */
1218
+ valueFor(placeholder) {
1219
+ switch (placeholder) {
1220
+ case 'projectName':
1221
+ return this.projectName();
1222
+ case 'primaryEntity':
1223
+ return this.primaryEntity();
1224
+ default:
1225
+ return null;
1226
+ }
1227
+ }
1228
+ interpolate(template) {
1229
+ return template.replace(/{{\s*(\w+)\s*}}/g, (full, key) => {
1230
+ const value = this.valueFor(key);
1231
+ // Unknown placeholders survive verbatim so a typo in the catalogue
1232
+ // surfaces in the preview rather than silently disappearing.
1233
+ return value !== null ? value.trim() : full;
1234
+ });
1235
+ }
1236
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiNewAppDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1237
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiNewAppDialogComponent, isStandalone: true, selector: "mm-ai-new-app-dialog", inputs: { templates: { classPropertyName: "templates", publicName: "templates", isSignal: true, isRequired: true, transformFunction: null }, tenantName: { classPropertyName: "tenantName", publicName: "tenantName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { confirmed: "confirmed", cancelled: "cancelled" }, ngImport: i0, template: "<div class=\"mm-ai-new-app\">\n <header class=\"mm-ai-new-app__header\">\n <h3 class=\"mm-ai-new-app__title\">\n New App\n @if (tenantName(); as tenant) {\n <span class=\"mm-ai-new-app__tenant\"> \u00B7 {{ tenant }}</span>\n }\n </h3>\n <button\n type=\"button\"\n class=\"mm-ai-new-app__close\"\n (click)=\"onCancel()\"\n aria-label=\"Dismiss\"\n >\n \u00D7\n </button>\n </header>\n\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">Template</h4>\n <ul class=\"mm-ai-new-app__templates\" role=\"radiogroup\" aria-label=\"Template\">\n @for (template of templates(); track template.id) {\n <li>\n <label\n class=\"mm-ai-new-app__template\"\n [class.mm-ai-new-app__template--active]=\"\n selectedTemplateId() === template.id\n \"\n >\n <input\n type=\"radio\"\n name=\"mm-ai-new-app-template\"\n [value]=\"template.id\"\n [checked]=\"selectedTemplateId() === template.id\"\n (change)=\"selectedTemplateId.set(template.id)\"\n />\n <span class=\"mm-ai-new-app__template-body\">\n <strong class=\"mm-ai-new-app__template-name\">{{ template.name }}</strong>\n <span class=\"mm-ai-new-app__template-description\">\n {{ template.description }}\n </span>\n </span>\n </label>\n </li>\n }\n </ul>\n </div>\n\n @if (selectedTemplate(); as template) {\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">Parameters</h4>\n <div class=\"mm-ai-new-app__field\">\n <label for=\"mm-ai-new-app-project-name\">Project name</label>\n <input\n id=\"mm-ai-new-app-project-name\"\n type=\"text\"\n [ngModel]=\"projectName()\"\n (ngModelChange)=\"projectName.set($event)\"\n placeholder=\"e.g. customer-list\"\n autocomplete=\"off\"\n />\n </div>\n <div class=\"mm-ai-new-app__field\">\n <label for=\"mm-ai-new-app-primary-entity\">Primary entity (CK type)</label>\n <input\n id=\"mm-ai-new-app-primary-entity\"\n type=\"text\"\n [ngModel]=\"primaryEntity()\"\n (ngModelChange)=\"primaryEntity.set($event)\"\n placeholder=\"e.g. Customer\"\n autocomplete=\"off\"\n />\n </div>\n </div>\n\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">\n Generated Goal <span>(this is what Claude will see)</span>\n </h4>\n <pre class=\"mm-ai-new-app__preview\">{{\n renderedGoal() ||\n 'Fill in every field above to preview the Goal that will be sent.'\n }}</pre>\n </div>\n }\n\n <footer class=\"mm-ai-new-app__actions\">\n <button\n type=\"button\"\n class=\"mm-ai-new-app__btn mm-ai-new-app__btn--cancel\"\n (click)=\"onCancel()\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mm-ai-new-app__btn mm-ai-new-app__btn--submit\"\n [disabled]=\"!allRequiredFilled()\"\n (click)=\"onSubmit()\"\n >\n Create App Session\n </button>\n </footer>\n</div>\n", styles: [":host{display:block;--mm-ai-new-app-bg: #1f2227;--mm-ai-new-app-border: #2f343b;--mm-ai-new-app-text: #e6e6e6;--mm-ai-new-app-muted: #9aa0a6;--mm-ai-new-app-accent: #2ea44f;--mm-ai-new-app-accent-soft: rgba(46, 164, 79, .15);--mm-ai-new-app-cancel: #44494f;--mm-ai-new-app-code-bg: #16191d}.mm-ai-new-app{background:var(--mm-ai-new-app-bg);color:var(--mm-ai-new-app-text);border:1px solid var(--mm-ai-new-app-border);border-radius:.6rem;padding:1rem 1.25rem;width:min(40rem,100%)}.mm-ai-new-app__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.mm-ai-new-app__title{margin:0;font-size:1rem}.mm-ai-new-app__tenant{color:var(--mm-ai-new-app-muted);font-weight:400}.mm-ai-new-app__close{background:transparent;color:var(--mm-ai-new-app-muted);border:none;font-size:1.4rem;cursor:pointer;line-height:1;padding:0 .25rem}.mm-ai-new-app__close:hover{color:var(--mm-ai-new-app-text)}.mm-ai-new-app__section{margin-bottom:.75rem}.mm-ai-new-app__heading{margin:0 0 .35rem;font-size:.75rem;color:var(--mm-ai-new-app-muted);text-transform:uppercase;letter-spacing:.04em}.mm-ai-new-app__heading span{text-transform:none;letter-spacing:0;opacity:.6}.mm-ai-new-app__templates{list-style:none;margin:0;padding:0;display:grid;gap:.4rem}.mm-ai-new-app__template{display:flex;gap:.6rem;padding:.55rem .7rem;border:1px solid var(--mm-ai-new-app-border);border-radius:.35rem;cursor:pointer;align-items:flex-start}.mm-ai-new-app__template--active{background:var(--mm-ai-new-app-accent-soft);border-color:var(--mm-ai-new-app-accent)}.mm-ai-new-app__template input[type=radio]{margin-top:.2rem}.mm-ai-new-app__template-body{display:grid;gap:.2rem}.mm-ai-new-app__template-name{font-size:.9rem}.mm-ai-new-app__template-description{font-size:.8rem;color:var(--mm-ai-new-app-muted)}.mm-ai-new-app__field{display:grid;gap:.25rem;margin-bottom:.5rem;font-size:.85rem}.mm-ai-new-app__field label{color:var(--mm-ai-new-app-muted)}.mm-ai-new-app__field input{padding:.45rem .55rem;background:var(--mm-ai-new-app-code-bg);color:inherit;border:1px solid var(--mm-ai-new-app-border);border-radius:.3rem;font-family:inherit;font-size:.9rem}.mm-ai-new-app__preview{margin:0;padding:.5rem .6rem;background:var(--mm-ai-new-app-code-bg);border-radius:.3rem;font-size:.8rem;white-space:pre-wrap;word-break:break-word;max-height:12rem;overflow-y:auto}.mm-ai-new-app__actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem}.mm-ai-new-app__btn{padding:.45rem 1rem;border-radius:.3rem;font-weight:600;border:none;cursor:pointer;font-size:.85rem}.mm-ai-new-app__btn--submit{background:var(--mm-ai-new-app-accent);color:#fff}.mm-ai-new-app__btn--submit:disabled{opacity:.5;cursor:not-allowed}.mm-ai-new-app__btn--cancel{background:var(--mm-ai-new-app-cancel);color:#fff}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1238
+ }
1239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiNewAppDialogComponent, decorators: [{
1240
+ type: Component,
1241
+ args: [{ selector: 'mm-ai-new-app-dialog', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-new-app\">\n <header class=\"mm-ai-new-app__header\">\n <h3 class=\"mm-ai-new-app__title\">\n New App\n @if (tenantName(); as tenant) {\n <span class=\"mm-ai-new-app__tenant\"> \u00B7 {{ tenant }}</span>\n }\n </h3>\n <button\n type=\"button\"\n class=\"mm-ai-new-app__close\"\n (click)=\"onCancel()\"\n aria-label=\"Dismiss\"\n >\n \u00D7\n </button>\n </header>\n\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">Template</h4>\n <ul class=\"mm-ai-new-app__templates\" role=\"radiogroup\" aria-label=\"Template\">\n @for (template of templates(); track template.id) {\n <li>\n <label\n class=\"mm-ai-new-app__template\"\n [class.mm-ai-new-app__template--active]=\"\n selectedTemplateId() === template.id\n \"\n >\n <input\n type=\"radio\"\n name=\"mm-ai-new-app-template\"\n [value]=\"template.id\"\n [checked]=\"selectedTemplateId() === template.id\"\n (change)=\"selectedTemplateId.set(template.id)\"\n />\n <span class=\"mm-ai-new-app__template-body\">\n <strong class=\"mm-ai-new-app__template-name\">{{ template.name }}</strong>\n <span class=\"mm-ai-new-app__template-description\">\n {{ template.description }}\n </span>\n </span>\n </label>\n </li>\n }\n </ul>\n </div>\n\n @if (selectedTemplate(); as template) {\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">Parameters</h4>\n <div class=\"mm-ai-new-app__field\">\n <label for=\"mm-ai-new-app-project-name\">Project name</label>\n <input\n id=\"mm-ai-new-app-project-name\"\n type=\"text\"\n [ngModel]=\"projectName()\"\n (ngModelChange)=\"projectName.set($event)\"\n placeholder=\"e.g. customer-list\"\n autocomplete=\"off\"\n />\n </div>\n <div class=\"mm-ai-new-app__field\">\n <label for=\"mm-ai-new-app-primary-entity\">Primary entity (CK type)</label>\n <input\n id=\"mm-ai-new-app-primary-entity\"\n type=\"text\"\n [ngModel]=\"primaryEntity()\"\n (ngModelChange)=\"primaryEntity.set($event)\"\n placeholder=\"e.g. Customer\"\n autocomplete=\"off\"\n />\n </div>\n </div>\n\n <div class=\"mm-ai-new-app__section\">\n <h4 class=\"mm-ai-new-app__heading\">\n Generated Goal <span>(this is what Claude will see)</span>\n </h4>\n <pre class=\"mm-ai-new-app__preview\">{{\n renderedGoal() ||\n 'Fill in every field above to preview the Goal that will be sent.'\n }}</pre>\n </div>\n }\n\n <footer class=\"mm-ai-new-app__actions\">\n <button\n type=\"button\"\n class=\"mm-ai-new-app__btn mm-ai-new-app__btn--cancel\"\n (click)=\"onCancel()\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mm-ai-new-app__btn mm-ai-new-app__btn--submit\"\n [disabled]=\"!allRequiredFilled()\"\n (click)=\"onSubmit()\"\n >\n Create App Session\n </button>\n </footer>\n</div>\n", styles: [":host{display:block;--mm-ai-new-app-bg: #1f2227;--mm-ai-new-app-border: #2f343b;--mm-ai-new-app-text: #e6e6e6;--mm-ai-new-app-muted: #9aa0a6;--mm-ai-new-app-accent: #2ea44f;--mm-ai-new-app-accent-soft: rgba(46, 164, 79, .15);--mm-ai-new-app-cancel: #44494f;--mm-ai-new-app-code-bg: #16191d}.mm-ai-new-app{background:var(--mm-ai-new-app-bg);color:var(--mm-ai-new-app-text);border:1px solid var(--mm-ai-new-app-border);border-radius:.6rem;padding:1rem 1.25rem;width:min(40rem,100%)}.mm-ai-new-app__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.mm-ai-new-app__title{margin:0;font-size:1rem}.mm-ai-new-app__tenant{color:var(--mm-ai-new-app-muted);font-weight:400}.mm-ai-new-app__close{background:transparent;color:var(--mm-ai-new-app-muted);border:none;font-size:1.4rem;cursor:pointer;line-height:1;padding:0 .25rem}.mm-ai-new-app__close:hover{color:var(--mm-ai-new-app-text)}.mm-ai-new-app__section{margin-bottom:.75rem}.mm-ai-new-app__heading{margin:0 0 .35rem;font-size:.75rem;color:var(--mm-ai-new-app-muted);text-transform:uppercase;letter-spacing:.04em}.mm-ai-new-app__heading span{text-transform:none;letter-spacing:0;opacity:.6}.mm-ai-new-app__templates{list-style:none;margin:0;padding:0;display:grid;gap:.4rem}.mm-ai-new-app__template{display:flex;gap:.6rem;padding:.55rem .7rem;border:1px solid var(--mm-ai-new-app-border);border-radius:.35rem;cursor:pointer;align-items:flex-start}.mm-ai-new-app__template--active{background:var(--mm-ai-new-app-accent-soft);border-color:var(--mm-ai-new-app-accent)}.mm-ai-new-app__template input[type=radio]{margin-top:.2rem}.mm-ai-new-app__template-body{display:grid;gap:.2rem}.mm-ai-new-app__template-name{font-size:.9rem}.mm-ai-new-app__template-description{font-size:.8rem;color:var(--mm-ai-new-app-muted)}.mm-ai-new-app__field{display:grid;gap:.25rem;margin-bottom:.5rem;font-size:.85rem}.mm-ai-new-app__field label{color:var(--mm-ai-new-app-muted)}.mm-ai-new-app__field input{padding:.45rem .55rem;background:var(--mm-ai-new-app-code-bg);color:inherit;border:1px solid var(--mm-ai-new-app-border);border-radius:.3rem;font-family:inherit;font-size:.9rem}.mm-ai-new-app__preview{margin:0;padding:.5rem .6rem;background:var(--mm-ai-new-app-code-bg);border-radius:.3rem;font-size:.8rem;white-space:pre-wrap;word-break:break-word;max-height:12rem;overflow-y:auto}.mm-ai-new-app__actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem}.mm-ai-new-app__btn{padding:.45rem 1rem;border-radius:.3rem;font-weight:600;border:none;cursor:pointer;font-size:.85rem}.mm-ai-new-app__btn--submit{background:var(--mm-ai-new-app-accent);color:#fff}.mm-ai-new-app__btn--submit:disabled{opacity:.5;cursor:not-allowed}.mm-ai-new-app__btn--cancel{background:var(--mm-ai-new-app-cancel);color:#fff}\n"] }]
1242
+ }], propDecorators: { templates: [{ type: i0.Input, args: [{ isSignal: true, alias: "templates", required: true }] }], tenantName: [{ type: i0.Input, args: [{ isSignal: true, alias: "tenantName", required: false }] }], confirmed: [{ type: i0.Output, args: ["confirmed"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
1243
+
677
1244
  /*
678
1245
  * Public API Surface of @meshmakers/octo-ai-console
679
1246
  *
@@ -686,5 +1253,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
686
1253
  * Generated bundle index. Do not edit.
687
1254
  */
688
1255
 
689
- export { AI_ADAPTER_OPTIONS, AI_SESSION_STREAM_CONNECTION_FACTORY, AiAdapterClientService, AiApprovalModalComponent, AiChatStreamComponent, AiCredentialTicketIssueComponent, AiJobStatusBadgeComponent, AiQuotaIndicatorComponent, AiSessionListComponent, AiSessionStreamService, AiToolCallComponent, provideOctoAiConsole };
1256
+ export { AI_ADAPTER_OPTIONS, AI_SESSION_STREAM_CONNECTION_FACTORY, AiAdapterClientService, AiApprovalModalComponent, AiChatStreamComponent, AiCredentialTicketIssueComponent, AiJobStatusBadgeComponent, AiNewAppDialogComponent, AiQuotaIndicatorComponent, AiSessionListComponent, AiSessionStreamService, AiToolCallComponent, provideOctoAiConsole };
690
1257
  //# sourceMappingURL=meshmakers-octo-ai-console.mjs.map