@meshmakers/octo-ai-console 3.3.1200 → 3.4.50
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 <id></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-
|
|
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-
|
|
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
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
|
|
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
|
|
381
|
-
* the
|
|
382
|
-
*
|
|
383
|
-
*
|
|
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
|
|
390
|
-
*
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|