@threadplane/chat 0.0.55 → 0.0.56

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, input, inject, TemplateRef, Directive, contentChildren, computed, ChangeDetectionStrategy, Component, signal, Injectable, ContentChild, effect, output, DOCUMENT, ViewEncapsulation, viewChild, model, contentChild, untracked, ElementRef, ViewContainerRef, DestroyRef, makeEnvironmentProviders, Injector, runInInjectionContext, SecurityContext } from '@angular/core';
2
+ import { InjectionToken, input, inject, TemplateRef, Directive, contentChildren, computed, ChangeDetectionStrategy, Component, signal, Injectable, ContentChild, effect, output, DOCUMENT, ViewEncapsulation, ElementRef, ViewContainerRef, DestroyRef, viewChild, model, contentChild, untracked, makeEnvironmentProviders, Injector, runInInjectionContext, SecurityContext } from '@angular/core';
3
3
  import { NgTemplateOutlet, NgComponentOutlet, KeyValuePipe } from '@angular/common';
4
4
  import { createPartialMarkdownParser, materialize } from '@cacheplane/partial-markdown';
5
5
  import { views, RenderSpecComponent, toRenderRegistry, signalStateStore, withViews, RenderElementComponent, injectRenderHost } from '@threadplane/render';
@@ -305,6 +305,14 @@ const LIGHT_TOKENS = `
305
305
  --a2ui-elevation-3: 0 4px 8px rgba(0, 0, 0, 0.10);
306
306
  --a2ui-elevation-4: 0 8px 16px rgba(0, 0, 0, 0.14);
307
307
  --a2ui-elevation-5: 0 16px 32px rgba(0, 0, 0, 0.18);
308
+
309
+ /* --tplane-chat-citation-* — inline markers, preview card, sources panel */
310
+ --tplane-chat-citation-accent: #2f6fe0;
311
+ --tplane-chat-citation-accent-soft: #eaf1fd;
312
+ --tplane-chat-citation-accent-border: #c9def8;
313
+ --tplane-chat-citation-marker-bg: #f1f2f4;
314
+ --tplane-chat-citation-marker-border: var(--tplane-chat-separator);
315
+ --tplane-chat-citation-marker-fg: #4b5563;
308
316
  `;
309
317
  const DARK_TOKENS = `
310
318
  --tplane-chat-bg: rgb(17, 17, 17);
@@ -345,6 +353,14 @@ const DARK_TOKENS = `
345
353
  --a2ui-elevation-3: 0 4px 8px rgba(0, 0, 0, 0.4);
346
354
  --a2ui-elevation-4: 0 8px 16px rgba(0, 0, 0, 0.45);
347
355
  --a2ui-elevation-5: 0 16px 32px rgba(0, 0, 0, 0.5);
356
+
357
+ /* --tplane-chat-citation-* dark variant */
358
+ --tplane-chat-citation-accent: #6ea8ff;
359
+ --tplane-chat-citation-accent-soft: rgba(79, 141, 245, 0.16);
360
+ --tplane-chat-citation-accent-border: rgba(79, 141, 245, 0.38);
361
+ --tplane-chat-citation-marker-bg: rgba(255, 255, 255, 0.08);
362
+ --tplane-chat-citation-marker-border: var(--tplane-chat-separator);
363
+ --tplane-chat-citation-marker-fg: #c9ccd1;
348
364
  `;
349
365
  const GEOMETRY_TOKENS = `
350
366
  --tplane-chat-radius-bubble: 15px;
@@ -353,6 +369,7 @@ const GEOMETRY_TOKENS = `
353
369
  --tplane-chat-radius-button: 8px;
354
370
  --tplane-chat-radius-launcher: 9999px;
355
371
  --tplane-chat-max-width: 48rem;
372
+ --tplane-chat-citation-radius: 6px;
356
373
  `;
357
374
  const TYPOGRAPHY_TOKENS = `
358
375
  --tplane-chat-font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
@@ -837,53 +854,364 @@ const CHAT_MESSAGE_STYLES = `
837
854
  .chat-message__control-btn svg { width: 16px; height: 16px; pointer-events: none; }
838
855
  `;
839
856
 
857
+ /** Hostname of `url` with a leading `www.` removed; null if absent/malformed. */
858
+ function deriveDomain(url) {
859
+ if (!url)
860
+ return null;
861
+ try {
862
+ return new URL(url).hostname.replace(/^www\./, '');
863
+ }
864
+ catch {
865
+ return null;
866
+ }
867
+ }
868
+ /** Explicit `sourceType`, else 'web' inferred from a url, else 'unknown'. */
869
+ function deriveSourceType(c) {
870
+ if (c.sourceType)
871
+ return c.sourceType;
872
+ return c.url ? 'web' : 'unknown';
873
+ }
874
+ /** Uppercased first letter of the domain (or title) for the monogram chip. */
875
+ function deriveMonogram(c) {
876
+ const seed = deriveDomain(c.url) ?? c.title ?? '';
877
+ const ch = seed.trim().charAt(0);
878
+ return ch ? ch.toUpperCase() : '?';
879
+ }
880
+ /** Deterministic hue in [0,360) from a seed string (stable monogram color). */
881
+ function monogramHue(seed) {
882
+ let h = 0;
883
+ for (let i = 0; i < seed.length; i++) {
884
+ h = (h * 31 + seed.charCodeAt(i)) % 360;
885
+ }
886
+ return h;
887
+ }
888
+ /** Short freshness label (e.g. "Apr 2024"); null when absent or unparseable. */
889
+ function formatPublished(value) {
890
+ if (value == null)
891
+ return null;
892
+ const d = value instanceof Date ? value : new Date(value);
893
+ if (Number.isNaN(d.getTime()))
894
+ return null;
895
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short' });
896
+ }
897
+ /** Deterministic monogram-chip background color (stable per source). */
898
+ function monogramColor(c) {
899
+ const seed = deriveDomain(c.url) ?? c.title ?? '?';
900
+ return `hsl(${monogramHue(seed)} 60% 45%)`;
901
+ }
902
+ /** Human label for the source-type badge; null when type is 'unknown'. */
903
+ function citationTypeLabel(c) {
904
+ const t = deriveSourceType(c);
905
+ if (t === 'unknown')
906
+ return null;
907
+ return t === 'web' ? 'Web' : t.charAt(0).toUpperCase() + t.slice(1);
908
+ }
909
+
910
+ // libs/chat/src/lib/styles/chat-citations.styles.ts
911
+ // SPDX-License-Identifier: MIT
912
+ /** Inline pill marker (chat-md-citation-reference). */
913
+ const CHAT_CITATION_MARKER_STYLES = `
914
+ :host { display: inline; }
915
+ .chat-citation-marker {
916
+ display: inline-flex;
917
+ align-items: center;
918
+ justify-content: center;
919
+ min-width: 17px;
920
+ height: 17px;
921
+ padding: 0 5px;
922
+ margin: 0 1px;
923
+ font-size: 11px;
924
+ font-weight: 600;
925
+ line-height: 1;
926
+ color: var(--tplane-chat-citation-marker-fg);
927
+ background: var(--tplane-chat-citation-marker-bg);
928
+ border: 1px solid var(--tplane-chat-citation-marker-border);
929
+ border-radius: var(--tplane-chat-citation-radius);
930
+ translate: 0 -1px;
931
+ text-decoration: none;
932
+ cursor: pointer;
933
+ white-space: nowrap;
934
+ transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
935
+ }
936
+ .chat-citation-marker:hover,
937
+ .chat-citation-marker:focus-visible {
938
+ background: var(--tplane-chat-citation-accent-soft);
939
+ border-color: var(--tplane-chat-citation-accent-border);
940
+ color: var(--tplane-chat-citation-accent);
941
+ outline: none;
942
+ }
943
+ .chat-citation-marker:focus-visible {
944
+ box-shadow: 0 0 0 2px var(--tplane-chat-citation-accent-border);
945
+ }
946
+ .chat-citation-marker--unresolved {
947
+ color: var(--tplane-chat-text-muted);
948
+ background: transparent;
949
+ border-style: dashed;
950
+ cursor: default;
951
+ }
952
+ .chat-citation-marker--unresolved:hover {
953
+ background: transparent;
954
+ border-color: var(--tplane-chat-citation-marker-border);
955
+ color: var(--tplane-chat-text-muted);
956
+ }
957
+ .chat-citation-marker--no-url {
958
+ cursor: help;
959
+ }
960
+ `;
961
+ /** Provenance preview card (chat-citation-preview), portaled into the overlay pane. */
962
+ const CHAT_CITATION_PREVIEW_STYLES = `
963
+ :host { display: block; }
964
+ .chat-citation-preview {
965
+ width: 320px;
966
+ max-width: calc(100vw - 24px);
967
+ box-sizing: border-box;
968
+ background: var(--tplane-chat-surface);
969
+ border: 1px solid var(--tplane-chat-separator);
970
+ border-radius: var(--tplane-chat-radius-card);
971
+ box-shadow: var(--tplane-chat-shadow-md);
972
+ padding: 12px 13px;
973
+ text-align: left;
974
+ color: var(--tplane-chat-text);
975
+ }
976
+ .chat-citation-preview__head {
977
+ display: flex;
978
+ align-items: center;
979
+ gap: 7px;
980
+ margin-bottom: 7px;
981
+ }
982
+ .chat-citation-preview__fav {
983
+ width: 16px; height: 16px;
984
+ border-radius: 4px;
985
+ flex: 0 0 auto;
986
+ object-fit: cover;
987
+ }
988
+ .chat-citation-preview__fav--mono {
989
+ display: flex; align-items: center; justify-content: center;
990
+ color: #fff; font-size: 10px; font-weight: 700;
991
+ }
992
+ .chat-citation-preview__domain {
993
+ font-size: 12px;
994
+ color: var(--tplane-chat-text-muted);
995
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
996
+ }
997
+ .chat-citation-preview__type {
998
+ margin-left: auto;
999
+ font-size: 11px;
1000
+ color: var(--tplane-chat-text-muted);
1001
+ flex: 0 0 auto;
1002
+ }
1003
+ .chat-citation-preview__title {
1004
+ font-size: 14px; font-weight: 600; line-height: 1.35;
1005
+ margin: 0 0 5px;
1006
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
1007
+ }
1008
+ .chat-citation-preview__snippet {
1009
+ font-size: 12.5px; color: var(--tplane-chat-text-muted); line-height: 1.5;
1010
+ margin: 0;
1011
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
1012
+ }
1013
+ .chat-citation-preview__foot {
1014
+ display: flex; align-items: center; justify-content: space-between;
1015
+ margin-top: 10px; padding-top: 9px;
1016
+ border-top: 1px solid var(--tplane-chat-separator);
1017
+ }
1018
+ .chat-citation-preview__open {
1019
+ display: inline-flex; align-items: center; gap: 5px;
1020
+ font-size: 12px; font-weight: 600;
1021
+ color: var(--tplane-chat-citation-accent);
1022
+ text-decoration: none;
1023
+ }
1024
+ .chat-citation-preview__open:hover { text-decoration: underline; }
1025
+ .chat-citation-preview__meta { font-size: 11px; color: var(--tplane-chat-text-muted); }
1026
+ `;
1027
+ /** Sources panel (chat-citations) + detail card (chat-citations-card). */
1028
+ const CHAT_CITATIONS_PANEL_STYLES = `
1029
+ :host { display: block; }
1030
+ .chat-citations {
1031
+ margin-top: var(--tplane-chat-space-5);
1032
+ padding-top: var(--tplane-chat-space-4);
1033
+ border-top: 1px solid var(--tplane-chat-separator);
1034
+ }
1035
+ .chat-citations__header {
1036
+ display: flex; align-items: center; gap: 9px;
1037
+ width: 100%;
1038
+ padding: 0 0 11px;
1039
+ background: none; border: 0;
1040
+ font: inherit; color: inherit;
1041
+ cursor: pointer; text-align: left;
1042
+ }
1043
+ .chat-citations__heading { font-size: 13px; font-weight: 600; }
1044
+ .chat-citations__count {
1045
+ font-size: 11px; font-weight: 600;
1046
+ color: var(--tplane-chat-text-muted);
1047
+ background: var(--tplane-chat-surface-alt);
1048
+ border: 1px solid var(--tplane-chat-separator);
1049
+ border-radius: 20px;
1050
+ padding: 1px 7px;
1051
+ }
1052
+ .chat-citations__favstack { display: flex; margin-left: 2px; }
1053
+ .chat-citations__fav {
1054
+ width: 16px; height: 16px;
1055
+ border-radius: 4px;
1056
+ margin-left: -5px;
1057
+ border: 1.5px solid var(--tplane-chat-surface);
1058
+ object-fit: cover;
1059
+ }
1060
+ .chat-citations__fav:first-child { margin-left: 0; }
1061
+ .chat-citations__fav--mono {
1062
+ display: flex; align-items: center; justify-content: center;
1063
+ color: #fff; font-size: 9px; font-weight: 700;
1064
+ }
1065
+ .chat-citations__chevron {
1066
+ margin-left: auto;
1067
+ color: var(--tplane-chat-text-muted);
1068
+ transition: transform 120ms ease;
1069
+ }
1070
+ .chat-citations__chevron.is-open { transform: rotate(180deg); }
1071
+ .chat-citations__list {
1072
+ list-style: none; margin: 0; padding: 0;
1073
+ display: flex; flex-direction: column; gap: 8px;
1074
+ }
1075
+ .chat-citations__item { margin: 0; }
1076
+
1077
+ .chat-citations-card {
1078
+ display: flex; gap: 10px;
1079
+ padding: 10px 11px;
1080
+ background: var(--tplane-chat-surface);
1081
+ border: 1px solid var(--tplane-chat-separator);
1082
+ border-radius: var(--tplane-chat-radius-card);
1083
+ text-decoration: none; color: inherit;
1084
+ cursor: pointer;
1085
+ transition: border-color 120ms ease, background 120ms ease;
1086
+ }
1087
+ .chat-citations-card:hover,
1088
+ .chat-citations-card:focus-visible {
1089
+ border-color: var(--tplane-chat-citation-accent-border);
1090
+ outline: none;
1091
+ }
1092
+ .chat-citations-card__index {
1093
+ flex: 0 0 auto;
1094
+ width: 18px; height: 18px;
1095
+ border-radius: 5px;
1096
+ background: var(--tplane-chat-surface-alt);
1097
+ border: 1px solid var(--tplane-chat-separator);
1098
+ color: var(--tplane-chat-text-muted);
1099
+ font-size: 11px; font-weight: 600;
1100
+ display: flex; align-items: center; justify-content: center;
1101
+ margin-top: 1px;
1102
+ }
1103
+ .chat-citations-card__body { min-width: 0; flex: 1; }
1104
+ .chat-citations-card__top {
1105
+ display: flex; align-items: center; gap: 6px; margin-bottom: 3px;
1106
+ }
1107
+ .chat-citations-card__fav {
1108
+ width: 14px; height: 14px; border-radius: 3px; flex: 0 0 auto; object-fit: cover;
1109
+ }
1110
+ .chat-citations-card__fav--mono {
1111
+ display: flex; align-items: center; justify-content: center;
1112
+ color: #fff; font-size: 8px; font-weight: 700;
1113
+ }
1114
+ .chat-citations-card__domain { font-size: 11.5px; color: var(--tplane-chat-text-muted); }
1115
+ .chat-citations-card__type { margin-left: auto; font-size: 10.5px; color: var(--tplane-chat-text-muted); }
1116
+ .chat-citations-card__title {
1117
+ font-size: 13.5px; font-weight: 600; line-height: 1.35;
1118
+ margin: 0 0 2px;
1119
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1120
+ }
1121
+ .chat-citations-card:hover .chat-citations-card__title { color: var(--tplane-chat-citation-accent); }
1122
+ .chat-citations-card__snippet {
1123
+ font-size: 12px; color: var(--tplane-chat-text-muted); line-height: 1.45;
1124
+ margin: 0;
1125
+ display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
1126
+ }
1127
+ `;
1128
+
840
1129
  // libs/chat/src/lib/primitives/chat-citations/chat-citations-card.component.ts
841
1130
  // SPDX-License-Identifier: MIT
1131
+ /**
1132
+ * Sources-panel detail card: index badge, favicon/monogram + domain + type,
1133
+ * title, one-line snippet. Renders as an <a> (opens the source) when a url is
1134
+ * present, otherwise a non-interactive <div>. Shares the panel style module.
1135
+ */
842
1136
  class ChatCitationsCardComponent {
843
1137
  citation = input.required(...(ngDevMode ? [{ debugName: "citation" }] : []));
1138
+ domain = computed(() => deriveDomain(this.citation().url), ...(ngDevMode ? [{ debugName: "domain" }] : []));
1139
+ title = computed(() => this.citation().title ?? this.citation().url ?? null, ...(ngDevMode ? [{ debugName: "title" }] : []));
1140
+ monogram = computed(() => deriveMonogram(this.citation()), ...(ngDevMode ? [{ debugName: "monogram" }] : []));
1141
+ monoColor = computed(() => monogramColor(this.citation()), ...(ngDevMode ? [{ debugName: "monoColor" }] : []));
1142
+ typeLabel = computed(() => citationTypeLabel(this.citation()), ...(ngDevMode ? [{ debugName: "typeLabel" }] : []));
844
1143
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationsCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
845
1144
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatCitationsCardComponent, isStandalone: true, selector: "chat-citations-card", inputs: { citation: { classPropertyName: "citation", publicName: "citation", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
846
- <div class="chat-citations-card">
847
- <div class="chat-citations-card__index">{{ citation().index }}</div>
848
- <div class="chat-citations-card__body">
849
- @if (citation().url; as url) {
850
- <a class="chat-citations-card__title" [href]="url" target="_blank" rel="noopener noreferrer">
851
- {{ citation().title ?? url }}
852
- </a>
853
- } @else if (citation().title) {
854
- <span class="chat-citations-card__title">{{ citation().title }}</span>
1145
+ @if (citation().url; as url) {
1146
+ <a class="chat-citations-card" [href]="url" target="_blank" rel="noopener noreferrer"
1147
+ [attr.aria-label]="'Source ' + citation().index + ': ' + (citation().title ?? url)">
1148
+ <ng-container [ngTemplateOutlet]="inner" />
1149
+ </a>
1150
+ } @else {
1151
+ <div class="chat-citations-card">
1152
+ <ng-container [ngTemplateOutlet]="inner" />
1153
+ </div>
1154
+ }
1155
+
1156
+ <ng-template #inner>
1157
+ <span class="chat-citations-card__index">{{ citation().index }}</span>
1158
+ <span class="chat-citations-card__body">
1159
+ <span class="chat-citations-card__top">
1160
+ @if (citation().iconUrl; as icon) {
1161
+ <img class="chat-citations-card__fav" [src]="icon" alt="" width="14" height="14" />
1162
+ } @else {
1163
+ <span class="chat-citations-card__fav chat-citations-card__fav--mono"
1164
+ [style.background]="monoColor()">{{ monogram() }}</span>
1165
+ }
1166
+ @if (domain(); as d) { <span class="chat-citations-card__domain">{{ d }}</span> }
1167
+ @if (typeLabel(); as t) { <span class="chat-citations-card__type">{{ t }}</span> }
1168
+ </span>
1169
+ @if (title(); as t) {
1170
+ <span class="chat-citations-card__title">{{ t }}</span>
855
1171
  }
856
1172
  @if (citation().snippet; as s) {
857
- <p class="chat-citations-card__snippet">{{ s }}</p>
1173
+ <span class="chat-citations-card__snippet">{{ s }}</span>
858
1174
  }
859
- </div>
860
- </div>
861
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1175
+ </span>
1176
+ </ng-template>
1177
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citations{margin-top:var(--tplane-chat-space-5);padding-top:var(--tplane-chat-space-4);border-top:1px solid var(--tplane-chat-separator)}.chat-citations__header{display:flex;align-items:center;gap:9px;width:100%;padding:0 0 11px;background:none;border:0;font:inherit;color:inherit;cursor:pointer;text-align:left}.chat-citations__heading{font-size:13px;font-weight:600}.chat-citations__count{font-size:11px;font-weight:600;color:var(--tplane-chat-text-muted);background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:20px;padding:1px 7px}.chat-citations__favstack{display:flex;margin-left:2px}.chat-citations__fav{width:16px;height:16px;border-radius:4px;margin-left:-5px;border:1.5px solid var(--tplane-chat-surface);object-fit:cover}.chat-citations__fav:first-child{margin-left:0}.chat-citations__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700}.chat-citations__chevron{margin-left:auto;color:var(--tplane-chat-text-muted);transition:transform .12s ease}.chat-citations__chevron.is-open{transform:rotate(180deg)}.chat-citations__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.chat-citations__item{margin:0}.chat-citations-card{display:flex;gap:10px;padding:10px 11px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s ease,background .12s ease}.chat-citations-card:hover,.chat-citations-card:focus-visible{border-color:var(--tplane-chat-citation-accent-border);outline:none}.chat-citations-card__index{flex:0 0 auto;width:18px;height:18px;border-radius:5px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center;margin-top:1px}.chat-citations-card__body{min-width:0;flex:1}.chat-citations-card__top{display:flex;align-items:center;gap:6px;margin-bottom:3px}.chat-citations-card__fav{width:14px;height:14px;border-radius:3px;flex:0 0 auto;object-fit:cover}.chat-citations-card__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:700}.chat-citations-card__domain{font-size:11.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__type{margin-left:auto;font-size:10.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__title{font-size:13.5px;font-weight:600;line-height:1.35;margin:0 0 2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citations-card:hover .chat-citations-card__title{color:var(--tplane-chat-citation-accent)}.chat-citations-card__snippet{font-size:12px;color:var(--tplane-chat-text-muted);line-height:1.45;margin:0;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
862
1178
  }
863
1179
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationsCardComponent, decorators: [{
864
1180
  type: Component,
865
- args: [{
866
- selector: 'chat-citations-card',
867
- standalone: true,
868
- changeDetection: ChangeDetectionStrategy.OnPush,
869
- template: `
870
- <div class="chat-citations-card">
871
- <div class="chat-citations-card__index">{{ citation().index }}</div>
872
- <div class="chat-citations-card__body">
873
- @if (citation().url; as url) {
874
- <a class="chat-citations-card__title" [href]="url" target="_blank" rel="noopener noreferrer">
875
- {{ citation().title ?? url }}
876
- </a>
877
- } @else if (citation().title) {
878
- <span class="chat-citations-card__title">{{ citation().title }}</span>
1181
+ args: [{ selector: 'chat-citations-card', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: `
1182
+ @if (citation().url; as url) {
1183
+ <a class="chat-citations-card" [href]="url" target="_blank" rel="noopener noreferrer"
1184
+ [attr.aria-label]="'Source ' + citation().index + ': ' + (citation().title ?? url)">
1185
+ <ng-container [ngTemplateOutlet]="inner" />
1186
+ </a>
1187
+ } @else {
1188
+ <div class="chat-citations-card">
1189
+ <ng-container [ngTemplateOutlet]="inner" />
1190
+ </div>
1191
+ }
1192
+
1193
+ <ng-template #inner>
1194
+ <span class="chat-citations-card__index">{{ citation().index }}</span>
1195
+ <span class="chat-citations-card__body">
1196
+ <span class="chat-citations-card__top">
1197
+ @if (citation().iconUrl; as icon) {
1198
+ <img class="chat-citations-card__fav" [src]="icon" alt="" width="14" height="14" />
1199
+ } @else {
1200
+ <span class="chat-citations-card__fav chat-citations-card__fav--mono"
1201
+ [style.background]="monoColor()">{{ monogram() }}</span>
1202
+ }
1203
+ @if (domain(); as d) { <span class="chat-citations-card__domain">{{ d }}</span> }
1204
+ @if (typeLabel(); as t) { <span class="chat-citations-card__type">{{ t }}</span> }
1205
+ </span>
1206
+ @if (title(); as t) {
1207
+ <span class="chat-citations-card__title">{{ t }}</span>
879
1208
  }
880
1209
  @if (citation().snippet; as s) {
881
- <p class="chat-citations-card__snippet">{{ s }}</p>
1210
+ <span class="chat-citations-card__snippet">{{ s }}</span>
882
1211
  }
883
- </div>
884
- </div>
885
- `,
886
- }]
1212
+ </span>
1213
+ </ng-template>
1214
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citations{margin-top:var(--tplane-chat-space-5);padding-top:var(--tplane-chat-space-4);border-top:1px solid var(--tplane-chat-separator)}.chat-citations__header{display:flex;align-items:center;gap:9px;width:100%;padding:0 0 11px;background:none;border:0;font:inherit;color:inherit;cursor:pointer;text-align:left}.chat-citations__heading{font-size:13px;font-weight:600}.chat-citations__count{font-size:11px;font-weight:600;color:var(--tplane-chat-text-muted);background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:20px;padding:1px 7px}.chat-citations__favstack{display:flex;margin-left:2px}.chat-citations__fav{width:16px;height:16px;border-radius:4px;margin-left:-5px;border:1.5px solid var(--tplane-chat-surface);object-fit:cover}.chat-citations__fav:first-child{margin-left:0}.chat-citations__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700}.chat-citations__chevron{margin-left:auto;color:var(--tplane-chat-text-muted);transition:transform .12s ease}.chat-citations__chevron.is-open{transform:rotate(180deg)}.chat-citations__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.chat-citations__item{margin:0}.chat-citations-card{display:flex;gap:10px;padding:10px 11px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s ease,background .12s ease}.chat-citations-card:hover,.chat-citations-card:focus-visible{border-color:var(--tplane-chat-citation-accent-border);outline:none}.chat-citations-card__index{flex:0 0 auto;width:18px;height:18px;border-radius:5px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center;margin-top:1px}.chat-citations-card__body{min-width:0;flex:1}.chat-citations-card__top{display:flex;align-items:center;gap:6px;margin-bottom:3px}.chat-citations-card__fav{width:14px;height:14px;border-radius:3px;flex:0 0 auto;object-fit:cover}.chat-citations-card__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:700}.chat-citations-card__domain{font-size:11.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__type{margin-left:auto;font-size:10.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__title{font-size:13.5px;font-weight:600;line-height:1.35;margin:0 0 2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citations-card:hover .chat-citations-card__title{color:var(--tplane-chat-citation-accent)}.chat-citations-card__snippet{font-size:12px;color:var(--tplane-chat-text-muted);line-height:1.45;margin:0;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}\n"] }]
887
1215
  }], propDecorators: { citation: [{ type: i0.Input, args: [{ isSignal: true, alias: "citation", required: true }] }] } });
888
1216
 
889
1217
  // libs/chat/src/lib/markdown/citations-resolver.service.ts
@@ -952,29 +1280,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
952
1280
  type: Directive,
953
1281
  args: [{ selector: 'ng-template[chatCitationCard]', standalone: true }]
954
1282
  }] });
1283
+ let nextCitationsId = 0;
955
1284
  class ChatCitationsComponent {
956
1285
  message = input.required(...(ngDevMode ? [{ debugName: "message" }] : []));
957
1286
  heading = input('Sources', ...(ngDevMode ? [{ debugName: "heading" }] : []));
1287
+ expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
1288
+ listId = `chat-citations-list-${nextCitationsId++}`;
958
1289
  cardTpl = null;
959
- /**
960
- * Optional resolver — present when chat-citations is rendered inside a
961
- * chat-message that provides CitationsResolverService (the standard
962
- * placement). When absent, the panel reads only Message.citations.
963
- */
964
1290
  resolver = inject(CitationsResolverService, { optional: true });
965
1291
  /**
966
1292
  * Combined citation list:
967
1293
  * 1. Message.citations (provider-populated, takes precedence by id)
968
- * 2. Markdown sidecar defs (Pandoc-formatted [^id]: lines), merged in
969
- * for any id not already present.
970
- *
971
- * Sorted by index ascending. This guarantees the sources panel surfaces
972
- * citations whether they come from message metadata, content syntax, or
973
- * both — matching the same precedence as inline-marker resolution.
1294
+ * 2. Markdown sidecar defs (Pandoc [^id]: lines), merged for unseen ids.
1295
+ * Sorted by index ascending.
974
1296
  */
975
1297
  citations = computed(() => {
976
1298
  const fromMessage = this.message().citations ?? [];
977
- const seenIds = new Set(fromMessage.map(c => c.id));
1299
+ const seenIds = new Set(fromMessage.map((c) => c.id));
978
1300
  const fromMarkdown = [];
979
1301
  const mdDefs = this.resolver?.markdownDefs();
980
1302
  if (mdDefs) {
@@ -985,52 +1307,100 @@ class ChatCitationsComponent {
985
1307
  }
986
1308
  return [...fromMessage, ...fromMarkdown].sort((a, b) => a.index - b.index);
987
1309
  }, ...(ngDevMode ? [{ debugName: "citations" }] : []));
1310
+ /** First 3 sources, mapped to favicon/monogram chips for the header preview. */
1311
+ favstack = computed(() => this.citations().slice(0, 3).map((c) => ({
1312
+ id: c.id, iconUrl: c.iconUrl, mono: deriveMonogram(c), color: monogramColor(c),
1313
+ })), ...(ngDevMode ? [{ debugName: "favstack" }] : []));
988
1314
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
989
1315
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatCitationsComponent, isStandalone: true, selector: "chat-citations", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "cardTpl", first: true, predicate: ChatCitationCardTemplateDirective, descendants: true }], ngImport: i0, template: `
990
1316
  @if (citations().length > 0) {
991
1317
  <section class="chat-citations">
992
- <h4 class="chat-citations__heading">{{ heading() }}</h4>
993
- <ul class="chat-citations__list">
994
- @for (c of citations(); track c.id) {
995
- <li class="chat-citations__item">
996
- @if (cardTpl) {
997
- <ng-container *ngTemplateOutlet="cardTpl.tpl; context: { $implicit: c }" />
1318
+ <button
1319
+ type="button"
1320
+ class="chat-citations__header"
1321
+ [attr.aria-expanded]="expanded()"
1322
+ [attr.aria-controls]="listId"
1323
+ (click)="expanded.set(!expanded())"
1324
+ >
1325
+ <span class="chat-citations__heading">{{ heading() }}</span>
1326
+ <span class="chat-citations__count">{{ citations().length }}</span>
1327
+ <span class="chat-citations__favstack" aria-hidden="true">
1328
+ @for (f of favstack(); track f.id) {
1329
+ @if (f.iconUrl) {
1330
+ <img class="chat-citations__fav" [src]="f.iconUrl" alt="" width="16" height="16" />
998
1331
  } @else {
999
- <chat-citations-card [citation]="c" />
1332
+ <span class="chat-citations__fav chat-citations__fav--mono"
1333
+ [style.background]="f.color">{{ f.mono }}</span>
1000
1334
  }
1001
- </li>
1002
- }
1003
- </ul>
1335
+ }
1336
+ </span>
1337
+ <svg class="chat-citations__chevron" [class.is-open]="expanded()"
1338
+ viewBox="0 0 24 24" aria-hidden="true" width="15" height="15">
1339
+ <path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" />
1340
+ </svg>
1341
+ </button>
1342
+ @if (expanded()) {
1343
+ <ul class="chat-citations__list" [id]="listId">
1344
+ @for (c of citations(); track c.id) {
1345
+ <li class="chat-citations__item">
1346
+ @if (cardTpl) {
1347
+ <ng-container *ngTemplateOutlet="cardTpl.tpl; context: { $implicit: c }" />
1348
+ } @else {
1349
+ <chat-citations-card [citation]="c" />
1350
+ }
1351
+ </li>
1352
+ }
1353
+ </ul>
1354
+ }
1004
1355
  </section>
1005
1356
  }
1006
- `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ChatCitationsCardComponent, selector: "chat-citations-card", inputs: ["citation"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1357
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citations{margin-top:var(--tplane-chat-space-5);padding-top:var(--tplane-chat-space-4);border-top:1px solid var(--tplane-chat-separator)}.chat-citations__header{display:flex;align-items:center;gap:9px;width:100%;padding:0 0 11px;background:none;border:0;font:inherit;color:inherit;cursor:pointer;text-align:left}.chat-citations__heading{font-size:13px;font-weight:600}.chat-citations__count{font-size:11px;font-weight:600;color:var(--tplane-chat-text-muted);background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:20px;padding:1px 7px}.chat-citations__favstack{display:flex;margin-left:2px}.chat-citations__fav{width:16px;height:16px;border-radius:4px;margin-left:-5px;border:1.5px solid var(--tplane-chat-surface);object-fit:cover}.chat-citations__fav:first-child{margin-left:0}.chat-citations__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700}.chat-citations__chevron{margin-left:auto;color:var(--tplane-chat-text-muted);transition:transform .12s ease}.chat-citations__chevron.is-open{transform:rotate(180deg)}.chat-citations__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.chat-citations__item{margin:0}.chat-citations-card{display:flex;gap:10px;padding:10px 11px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s ease,background .12s ease}.chat-citations-card:hover,.chat-citations-card:focus-visible{border-color:var(--tplane-chat-citation-accent-border);outline:none}.chat-citations-card__index{flex:0 0 auto;width:18px;height:18px;border-radius:5px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center;margin-top:1px}.chat-citations-card__body{min-width:0;flex:1}.chat-citations-card__top{display:flex;align-items:center;gap:6px;margin-bottom:3px}.chat-citations-card__fav{width:14px;height:14px;border-radius:3px;flex:0 0 auto;object-fit:cover}.chat-citations-card__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:700}.chat-citations-card__domain{font-size:11.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__type{margin-left:auto;font-size:10.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__title{font-size:13.5px;font-weight:600;line-height:1.35;margin:0 0 2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citations-card:hover .chat-citations-card__title{color:var(--tplane-chat-citation-accent)}.chat-citations-card__snippet{font-size:12px;color:var(--tplane-chat-text-muted);line-height:1.45;margin:0;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ChatCitationsCardComponent, selector: "chat-citations-card", inputs: ["citation"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1007
1358
  }
1008
1359
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationsComponent, decorators: [{
1009
1360
  type: Component,
1010
- args: [{
1011
- selector: 'chat-citations',
1012
- standalone: true,
1013
- imports: [NgTemplateOutlet, ChatCitationsCardComponent],
1014
- changeDetection: ChangeDetectionStrategy.OnPush,
1015
- template: `
1361
+ args: [{ selector: 'chat-citations', standalone: true, imports: [NgTemplateOutlet, ChatCitationsCardComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1016
1362
  @if (citations().length > 0) {
1017
1363
  <section class="chat-citations">
1018
- <h4 class="chat-citations__heading">{{ heading() }}</h4>
1019
- <ul class="chat-citations__list">
1020
- @for (c of citations(); track c.id) {
1021
- <li class="chat-citations__item">
1022
- @if (cardTpl) {
1023
- <ng-container *ngTemplateOutlet="cardTpl.tpl; context: { $implicit: c }" />
1364
+ <button
1365
+ type="button"
1366
+ class="chat-citations__header"
1367
+ [attr.aria-expanded]="expanded()"
1368
+ [attr.aria-controls]="listId"
1369
+ (click)="expanded.set(!expanded())"
1370
+ >
1371
+ <span class="chat-citations__heading">{{ heading() }}</span>
1372
+ <span class="chat-citations__count">{{ citations().length }}</span>
1373
+ <span class="chat-citations__favstack" aria-hidden="true">
1374
+ @for (f of favstack(); track f.id) {
1375
+ @if (f.iconUrl) {
1376
+ <img class="chat-citations__fav" [src]="f.iconUrl" alt="" width="16" height="16" />
1024
1377
  } @else {
1025
- <chat-citations-card [citation]="c" />
1378
+ <span class="chat-citations__fav chat-citations__fav--mono"
1379
+ [style.background]="f.color">{{ f.mono }}</span>
1026
1380
  }
1027
- </li>
1028
- }
1029
- </ul>
1381
+ }
1382
+ </span>
1383
+ <svg class="chat-citations__chevron" [class.is-open]="expanded()"
1384
+ viewBox="0 0 24 24" aria-hidden="true" width="15" height="15">
1385
+ <path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" />
1386
+ </svg>
1387
+ </button>
1388
+ @if (expanded()) {
1389
+ <ul class="chat-citations__list" [id]="listId">
1390
+ @for (c of citations(); track c.id) {
1391
+ <li class="chat-citations__item">
1392
+ @if (cardTpl) {
1393
+ <ng-container *ngTemplateOutlet="cardTpl.tpl; context: { $implicit: c }" />
1394
+ } @else {
1395
+ <chat-citations-card [citation]="c" />
1396
+ }
1397
+ </li>
1398
+ }
1399
+ </ul>
1400
+ }
1030
1401
  </section>
1031
1402
  }
1032
- `,
1033
- }]
1403
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citations{margin-top:var(--tplane-chat-space-5);padding-top:var(--tplane-chat-space-4);border-top:1px solid var(--tplane-chat-separator)}.chat-citations__header{display:flex;align-items:center;gap:9px;width:100%;padding:0 0 11px;background:none;border:0;font:inherit;color:inherit;cursor:pointer;text-align:left}.chat-citations__heading{font-size:13px;font-weight:600}.chat-citations__count{font-size:11px;font-weight:600;color:var(--tplane-chat-text-muted);background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:20px;padding:1px 7px}.chat-citations__favstack{display:flex;margin-left:2px}.chat-citations__fav{width:16px;height:16px;border-radius:4px;margin-left:-5px;border:1.5px solid var(--tplane-chat-surface);object-fit:cover}.chat-citations__fav:first-child{margin-left:0}.chat-citations__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:9px;font-weight:700}.chat-citations__chevron{margin-left:auto;color:var(--tplane-chat-text-muted);transition:transform .12s ease}.chat-citations__chevron.is-open{transform:rotate(180deg)}.chat-citations__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.chat-citations__item{margin:0}.chat-citations-card{display:flex;gap:10px;padding:10px 11px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s ease,background .12s ease}.chat-citations-card:hover,.chat-citations-card:focus-visible{border-color:var(--tplane-chat-citation-accent-border);outline:none}.chat-citations-card__index{flex:0 0 auto;width:18px;height:18px;border-radius:5px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center;margin-top:1px}.chat-citations-card__body{min-width:0;flex:1}.chat-citations-card__top{display:flex;align-items:center;gap:6px;margin-bottom:3px}.chat-citations-card__fav{width:14px;height:14px;border-radius:3px;flex:0 0 auto;object-fit:cover}.chat-citations-card__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:700}.chat-citations-card__domain{font-size:11.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__type{margin-left:auto;font-size:10.5px;color:var(--tplane-chat-text-muted)}.chat-citations-card__title{font-size:13.5px;font-weight:600;line-height:1.35;margin:0 0 2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citations-card:hover .chat-citations-card__title{color:var(--tplane-chat-citation-accent)}.chat-citations-card__snippet{font-size:12px;color:var(--tplane-chat-text-muted);line-height:1.45;margin:0;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}\n"] }]
1034
1404
  }], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], cardTpl: [{
1035
1405
  type: ContentChild,
1036
1406
  args: [ChatCitationCardTemplateDirective]
@@ -2397,1593 +2767,2127 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2397
2767
  }]
2398
2768
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2399
2769
 
2400
- // libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts
2770
+ // libs/chat/src/lib/primitives/overlay/overlay-container.ts
2401
2771
  // SPDX-License-Identifier: MIT
2402
- class MarkdownCitationReferenceComponent {
2403
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2404
- resolver = inject(CitationsResolverService);
2405
- resolved = computed(() => {
2406
- const lookup = this.resolver.lookup(this.node().refId);
2407
- return lookup();
2408
- }, ...(ngDevMode ? [{ debugName: "resolved" }] : []));
2409
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownCitationReferenceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2410
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownCitationReferenceComponent, isStandalone: true, selector: "chat-md-citation-reference", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2411
- @if (resolved(); as r) {
2412
- @if (r.citation.url; as href) {
2413
- <a class="chat-citation-marker"
2414
- [attr.href]="href"
2415
- [attr.title]="r.citation.snippet ?? href"
2416
- target="_blank" rel="noopener noreferrer">
2417
- <sup>[{{ node().index }}]</sup>
2418
- </a>
2419
- } @else {
2420
- <span class="chat-citation-marker chat-citation-marker--no-url"
2421
- [attr.title]="r.citation.snippet ?? r.citation.title ?? null">
2422
- <sup>[{{ node().index }}]</sup>
2423
- </span>
2424
- }
2425
- } @else {
2426
- <span class="chat-citation-marker chat-citation-marker--unresolved"
2427
- [attr.title]="'No source available'">
2428
- <sup>[{{ node().index }}]</sup>
2429
- </span>
2430
- }
2431
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
2772
+ const CONTAINER_CLASS = 'chat-overlay-container';
2773
+ const STYLE_ID = 'chat-overlay-structure';
2774
+ // Structural CSS, injected once into <head> (same pattern as ROOT_TOKEN_STYLES
2775
+ // in chat-tokens.ts) so consumers need not import any stylesheet.
2776
+ const STRUCTURE_CSS = `
2777
+ .${CONTAINER_CLASS} {
2778
+ position: fixed;
2779
+ inset: 0;
2780
+ z-index: 1000;
2781
+ pointer-events: none;
2432
2782
  }
2433
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownCitationReferenceComponent, decorators: [{
2434
- type: Component,
2435
- args: [{
2436
- selector: 'chat-md-citation-reference',
2437
- standalone: true,
2438
- changeDetection: ChangeDetectionStrategy.OnPush,
2439
- template: `
2440
- @if (resolved(); as r) {
2441
- @if (r.citation.url; as href) {
2442
- <a class="chat-citation-marker"
2443
- [attr.href]="href"
2444
- [attr.title]="r.citation.snippet ?? href"
2445
- target="_blank" rel="noopener noreferrer">
2446
- <sup>[{{ node().index }}]</sup>
2447
- </a>
2448
- } @else {
2449
- <span class="chat-citation-marker chat-citation-marker--no-url"
2450
- [attr.title]="r.citation.snippet ?? r.citation.title ?? null">
2451
- <sup>[{{ node().index }}]</sup>
2452
- </span>
2453
- }
2454
- } @else {
2455
- <span class="chat-citation-marker chat-citation-marker--unresolved"
2456
- [attr.title]="'No source available'">
2457
- <sup>[{{ node().index }}]</sup>
2458
- </span>
2783
+ .chat-overlay-pane {
2784
+ position: absolute;
2785
+ pointer-events: auto;
2786
+ }
2787
+ `;
2788
+ /** Returns the single shared overlay container appended to <body>, creating it
2789
+ * (and injecting structural CSS) on first call. */
2790
+ function getOverlayContainer(doc) {
2791
+ const existing = doc.querySelector('.' + CONTAINER_CLASS);
2792
+ if (existing)
2793
+ return existing;
2794
+ if (!doc.getElementById(STYLE_ID)) {
2795
+ const style = doc.createElement('style');
2796
+ style.id = STYLE_ID;
2797
+ style.textContent = STRUCTURE_CSS;
2798
+ doc.head.appendChild(style);
2459
2799
  }
2460
- `,
2461
- }]
2462
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2463
-
2464
- // libs/chat/src/lib/markdown/markdown-table-row.token.ts
2465
- // SPDX-License-Identifier: MIT
2466
- /**
2467
- * Provided by MarkdownTableRowComponent for header rows so that
2468
- * MarkdownTableCellComponent can render <th> instead of <td>.
2469
- * The value is a Signal<boolean> so that it tracks the row's isHeader reactively.
2470
- */
2471
- const IS_HEADER_ROW = new InjectionToken('IS_HEADER_ROW', {
2472
- providedIn: null,
2473
- factory: () => signal(false),
2474
- });
2800
+ const container = doc.createElement('div');
2801
+ container.className = CONTAINER_CLASS;
2802
+ doc.body.appendChild(container);
2803
+ return container;
2804
+ }
2475
2805
 
2476
- // libs/chat/src/lib/markdown/views/markdown-table-row.component.ts
2806
+ // libs/chat/src/lib/primitives/overlay/connected-position.ts
2477
2807
  // SPDX-License-Identifier: MIT
2478
- class MarkdownTableRowComponent {
2479
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2480
- registry = inject(MARKDOWN_VIEW_REGISTRY);
2481
- resolve(child) {
2482
- const entry = this.registry[child.type];
2483
- if (!entry)
2484
- return null;
2485
- return typeof entry === 'function' ? entry : entry.component;
2486
- }
2487
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2488
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableRowComponent, isStandalone: true, selector: "chat-md-table-row", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
2489
- {
2490
- provide: IS_HEADER_ROW,
2491
- useFactory: () => {
2492
- const comp = inject(MarkdownTableRowComponent);
2493
- return computed(() => comp.node().isHeader);
2494
- },
2495
- },
2496
- ], ngImport: i0, template: `
2497
- <tr class="chat-md-table-row" [class.chat-md-table-row--header]="node().isHeader">
2498
- @for (child of node().children; track $index) {
2499
- @let comp = resolve(child);
2500
- @if (comp) {
2501
- <ng-container *ngComponentOutlet="comp; inputs: { node: child }" />
2502
- }
2503
- }
2504
- </tr>
2505
- `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2808
+ //
2809
+ // Minimal port of CDK's FlexibleConnectedPositionStrategy fit logic
2810
+ // (~/repos/components/src/cdk/overlay/position/flexible-connected-position-strategy.ts):
2811
+ // _getOriginPoint + _getOverlayPoint + _getOverlayFit + _pushOverlayOnScreen.
2812
+ // Omits flexible-dimensions, grow-after-open, RTL, and virtual-keyboard handling.
2813
+ function originPoint(origin, pos) {
2814
+ const x = pos.originX === 'center' ? origin.left + origin.width / 2 : pos.originX === 'start' ? origin.left : origin.right;
2815
+ const y = pos.originY === 'center' ? origin.top + origin.height / 2 : pos.originY === 'top' ? origin.top : origin.bottom;
2816
+ return { x, y };
2506
2817
  }
2507
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableRowComponent, decorators: [{
2508
- type: Component,
2509
- args: [{
2510
- selector: 'chat-md-table-row',
2511
- standalone: true,
2512
- imports: [NgComponentOutlet],
2513
- changeDetection: ChangeDetectionStrategy.OnPush,
2514
- template: `
2515
- <tr class="chat-md-table-row" [class.chat-md-table-row--header]="node().isHeader">
2516
- @for (child of node().children; track $index) {
2517
- @let comp = resolve(child);
2518
- @if (comp) {
2519
- <ng-container *ngComponentOutlet="comp; inputs: { node: child }" />
2520
- }
2521
- }
2522
- </tr>
2523
- `,
2524
- providers: [
2525
- {
2526
- provide: IS_HEADER_ROW,
2527
- useFactory: () => {
2528
- const comp = inject(MarkdownTableRowComponent);
2529
- return computed(() => comp.node().isHeader);
2530
- },
2531
- },
2532
- ],
2533
- }]
2534
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2535
-
2536
- // libs/chat/src/lib/markdown/views/markdown-table.component.ts
2537
- // SPDX-License-Identifier: MIT
2538
- class MarkdownTableComponent {
2539
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2540
- headerRow = computed(() => {
2541
- const rows = this.node().children;
2542
- return rows.length > 0 && rows[0].isHeader ? rows[0] : null;
2543
- }, ...(ngDevMode ? [{ debugName: "headerRow" }] : []));
2544
- bodyRows = computed(() => {
2545
- const rows = this.node().children;
2546
- return rows[0]?.isHeader ? rows.slice(1) : rows;
2547
- }, ...(ngDevMode ? [{ debugName: "bodyRows" }] : []));
2548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2549
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableComponent, isStandalone: true, selector: "chat-md-table", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2550
- <table class="chat-md-table">
2551
- <thead>
2552
- @if (headerRow(); as row) {
2553
- <chat-md-table-row [node]="row" />
2554
- }
2555
- </thead>
2556
- <tbody>
2557
- @for (row of bodyRows(); track $index) {
2558
- <chat-md-table-row [node]="row" />
2559
- }
2560
- </tbody>
2561
- </table>
2562
- `, isInline: true, dependencies: [{ kind: "component", type: MarkdownTableRowComponent, selector: "chat-md-table-row", inputs: ["node"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2818
+ function overlayPoint(origin, size, pos) {
2819
+ let x = origin.x;
2820
+ if (pos.overlayX === 'center')
2821
+ x -= size.width / 2;
2822
+ else if (pos.overlayX === 'end')
2823
+ x -= size.width;
2824
+ let y = origin.y;
2825
+ if (pos.overlayY === 'center')
2826
+ y -= size.height / 2;
2827
+ else if (pos.overlayY === 'bottom')
2828
+ y -= size.height;
2829
+ return { x: x + (pos.offsetX ?? 0), y: y + (pos.offsetY ?? 0) };
2563
2830
  }
2564
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableComponent, decorators: [{
2565
- type: Component,
2566
- args: [{
2567
- selector: 'chat-md-table',
2568
- standalone: true,
2569
- imports: [MarkdownTableRowComponent],
2570
- changeDetection: ChangeDetectionStrategy.OnPush,
2571
- template: `
2572
- <table class="chat-md-table">
2573
- <thead>
2574
- @if (headerRow(); as row) {
2575
- <chat-md-table-row [node]="row" />
2576
- }
2577
- </thead>
2578
- <tbody>
2579
- @for (row of bodyRows(); track $index) {
2580
- <chat-md-table-row [node]="row" />
2831
+ function fitArea(point, size, viewport) {
2832
+ const left = point.x;
2833
+ const right = point.x + size.width;
2834
+ const top = point.y;
2835
+ const bottom = point.y + size.height;
2836
+ const visibleW = Math.max(0, Math.min(right, viewport.right) - Math.max(left, viewport.left));
2837
+ const visibleH = Math.max(0, Math.min(bottom, viewport.bottom) - Math.max(top, viewport.top));
2838
+ const fits = left >= viewport.left && right <= viewport.right && top >= viewport.top && bottom <= viewport.bottom;
2839
+ return { area: visibleW * visibleH, fits };
2840
+ }
2841
+ function pushOnScreen(point, size, viewport) {
2842
+ const maxLeft = viewport.right - size.width;
2843
+ const maxTop = viewport.bottom - size.height;
2844
+ return {
2845
+ x: Math.max(viewport.left, Math.min(point.x, maxLeft)),
2846
+ y: Math.max(viewport.top, Math.min(point.y, maxTop)),
2847
+ };
2848
+ }
2849
+ /** Sensible fallback when no positions are supplied: below the origin, start-aligned. */
2850
+ const DEFAULT_POSITION = { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' };
2851
+ function computeConnectedPosition(args) {
2852
+ const { originRect, overlaySize, viewport, positions } = args;
2853
+ // An empty positions array (the directive's input default) must still anchor to
2854
+ // the trigger rather than crash on best! / render at 0,0.
2855
+ const list = positions.length ? positions : [DEFAULT_POSITION];
2856
+ let best = null;
2857
+ for (const pos of list) {
2858
+ const point = overlayPoint(originPoint(originRect, pos), overlaySize, pos);
2859
+ const { area, fits } = fitArea(point, overlaySize, viewport);
2860
+ if (fits) {
2861
+ return { top: point.y, left: point.x, position: pos };
2581
2862
  }
2582
- </tbody>
2583
- </table>
2584
- `,
2585
- }]
2586
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2587
-
2588
- // libs/chat/src/lib/markdown/views/markdown-table-cell.component.ts
2589
- // SPDX-License-Identifier: MIT
2590
- class MarkdownTableCellComponent {
2591
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2592
- isHeaderRowToken = inject(IS_HEADER_ROW, { optional: true });
2593
- isHeader = computed(() => this.isHeaderRowToken ? this.isHeaderRowToken() : false, ...(ngDevMode ? [{ debugName: "isHeader" }] : []));
2594
- alignment = computed(() => this.node().alignment, ...(ngDevMode ? [{ debugName: "alignment" }] : []));
2595
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2596
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableCellComponent, isStandalone: true, selector: "chat-md-table-cell", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2597
- @if (isHeader()) {
2598
- <th class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
2599
- <chat-md-children [parent]="node()" />
2600
- </th>
2601
- } @else {
2602
- <td class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
2603
- <chat-md-children [parent]="node()" />
2604
- </td>
2863
+ if (!best || area > best.area)
2864
+ best = { point, pos, area };
2605
2865
  }
2606
- `, isInline: true, dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2866
+ const pushed = pushOnScreen(best.point, overlaySize, viewport);
2867
+ return { top: pushed.y, left: pushed.x, position: best.pos };
2868
+ }
2869
+ function narrowViewport(win, margin) {
2870
+ return {
2871
+ left: margin,
2872
+ top: margin,
2873
+ right: win.innerWidth - margin,
2874
+ bottom: win.innerHeight - margin,
2875
+ width: win.innerWidth - 2 * margin,
2876
+ height: win.innerHeight - 2 * margin,
2877
+ };
2607
2878
  }
2608
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableCellComponent, decorators: [{
2609
- type: Component,
2610
- args: [{
2611
- selector: 'chat-md-table-cell',
2612
- standalone: true,
2613
- imports: [MarkdownChildrenComponent],
2614
- changeDetection: ChangeDetectionStrategy.OnPush,
2615
- template: `
2616
- @if (isHeader()) {
2617
- <th class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
2618
- <chat-md-children [parent]="node()" />
2619
- </th>
2620
- } @else {
2621
- <td class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
2622
- <chat-md-children [parent]="node()" />
2623
- </td>
2624
- }
2625
- `,
2626
- }]
2627
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2628
2879
 
2629
- // libs/chat/src/lib/markdown/views/markdown-html.component.ts
2880
+ // libs/chat/src/lib/primitives/overlay/connected-overlay.directive.ts
2630
2881
  // SPDX-License-Identifier: MIT
2631
- /**
2632
- * Renders a `html-block` / `html-inline` markdown node as **escaped text**
2633
- * the raw HTML is shown literally (Angular interpolation auto-escapes it),
2634
- * never injected as live markup. This preserves the pre-0.4 behavior where
2635
- * raw HTML was plain text, and keeps the chat XSS-safe: model-emitted
2636
- * `<script>`, `<iframe>`, etc. are displayed as text and never executed.
2637
- */
2638
- class MarkdownHtmlComponent {
2639
- node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2640
- raw = computed(() => this.node().raw, ...(ngDevMode ? [{ debugName: "raw" }] : []));
2641
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: MarkdownHtmlComponent, isStandalone: true, selector: "chat-md-html", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `{{ raw() }}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
2882
+ /* eslint-disable @angular-eslint/no-input-rename, @angular-eslint/no-output-rename --
2883
+ * The `chatOverlay*` binding aliases ARE the intended public API: they namespace
2884
+ * the directive's inputs/outputs under the `chatOverlay` prefix (mirroring Angular
2885
+ * CDK's `cdkConnectedOverlay*` convention) rather than exposing bare names like
2886
+ * `open`/`positions` on an `<ng-template>`. The internal property names stay
2887
+ * concise, so aliasing here is deliberate, not a rename to avoid. */
2888
+ /** Marks the anchor element a connected overlay positions against. */
2889
+ class ChatOverlayOriginDirective {
2890
+ elementRef = inject(ElementRef);
2891
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverlayOriginDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2892
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.6", type: ChatOverlayOriginDirective, isStandalone: true, selector: "[chatOverlayOrigin]", exportAs: ["chatOverlayOrigin"], ngImport: i0 });
2643
2893
  }
2644
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, decorators: [{
2645
- type: Component,
2894
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverlayOriginDirective, decorators: [{
2895
+ type: Directive,
2646
2896
  args: [{
2647
- selector: 'chat-md-html',
2897
+ selector: '[chatOverlayOrigin]',
2648
2898
  standalone: true,
2649
- changeDetection: ChangeDetectionStrategy.OnPush,
2650
- template: `{{ raw() }}`,
2899
+ exportAs: 'chatOverlayOrigin',
2651
2900
  }]
2652
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2653
-
2654
- // libs/chat/src/lib/markdown/cacheplane-markdown-views.ts
2655
- // SPDX-License-Identifier: MIT
2901
+ }] });
2902
+ const VIEWPORT_MARGIN = 8;
2656
2903
  /**
2657
- * Default view registry consumed by <chat-streaming-md>. Maps every
2658
- * MarkdownNode.type emitted by @cacheplane/partial-markdown@0.2 to its
2659
- * corresponding Angular component.
2904
+ * Applied to an `<ng-template>`. When `chatOverlayOpen` is true, the template
2905
+ * content is portaled into the shared body-level overlay container and
2906
+ * positioned connected to `chatOverlayOrigin`, repositioning live on
2907
+ * scroll/resize. Closes on outside mousedown (via the `chatOverlayOutsideClick`
2908
+ * output) and Tab; returns focus to the origin when focus was inside the pane.
2660
2909
  *
2661
- * Override per-node-type via `withViews(cacheplaneMarkdownViews, { })`.
2910
+ * NOTE: the directive does not own the open state — it only emits. To fully
2911
+ * close the overlay, consumers MUST handle BOTH `(chatOverlayOutsideClick)` and
2912
+ * `(chatOverlayDetach)`. Tab-close (and Escape, when the consumer routes it)
2913
+ * surface through `chatOverlayDetach`, so wiring only `chatOverlayOutsideClick`
2914
+ * leaves the overlay stuck open on Tab.
2662
2915
  */
2663
- const cacheplaneMarkdownViews = views({
2664
- 'document': MarkdownDocumentComponent,
2665
- 'paragraph': MarkdownParagraphComponent,
2666
- 'heading': MarkdownHeadingComponent,
2667
- 'blockquote': MarkdownBlockquoteComponent,
2668
- 'list': MarkdownListComponent,
2669
- 'list-item': MarkdownListItemComponent,
2670
- 'code-block': MarkdownCodeBlockComponent,
2671
- 'thematic-break': MarkdownThematicBreakComponent,
2672
- 'text': MarkdownTextComponent,
2673
- 'emphasis': MarkdownEmphasisComponent,
2674
- 'strong': MarkdownStrongComponent,
2675
- 'strikethrough': MarkdownStrikethroughComponent,
2676
- 'inline-code': MarkdownInlineCodeComponent,
2677
- 'math-inline': MarkdownMathComponent,
2678
- 'math-display': MarkdownMathComponent,
2679
- 'link': MarkdownLinkComponent,
2680
- 'autolink': MarkdownAutolinkComponent,
2681
- 'image': MarkdownImageComponent,
2682
- 'soft-break': MarkdownSoftBreakComponent,
2683
- 'hard-break': MarkdownHardBreakComponent,
2684
- 'citation-reference': MarkdownCitationReferenceComponent,
2685
- 'table': MarkdownTableComponent,
2686
- 'table-row': MarkdownTableRowComponent,
2687
- 'table-cell': MarkdownTableCellComponent,
2688
- // Raw HTML (added by partial-markdown 0.4.x) renders as escaped literal text
2689
- // XSS-safe, and matches the pre-0.4 behavior where HTML was plain text.
2690
- 'html-block': MarkdownHtmlComponent,
2691
- 'html-inline': MarkdownHtmlComponent,
2692
- });
2693
-
2694
- // libs/chat/src/lib/streaming/streaming-markdown.component.ts
2695
- // SPDX-License-Identifier: MIT
2696
- // How long streaming must be false AND content stable before we finalize the
2697
- // parser. finish() is only needed to mark final node status (not used visually)
2698
- // and to revert a genuinely-truncated trailing construct to CommonMark — the
2699
- // live `parser.root` projection already renders everything during streaming, so
2700
- // this delay has NO visual cost. It must comfortably exceed real inter-chunk
2701
- // gaps (e.g. the pause between a table's header row and its delimiter row) and
2702
- // any `streaming` flag flap, so finalize never fires mid-stream and reverts an
2703
- // in-progress table to raw "| a | b |" text.
2704
- const FINALIZE_DEBOUNCE_MS = 600;
2705
- /**
2706
- * Renders streaming markdown by walking a @cacheplane/partial-markdown AST
2707
- * through @threadplane/render's view registry.
2708
- *
2709
- * Reactivity model: the live `parser.root` keeps a stable JS reference
2710
- * across pushes (partial-markdown's identity guarantee). To make Angular
2711
- * signals propagate downstream when the underlying tree changes, we surface
2712
- * a materialized snapshot via `materialize()`. The snapshot shares
2713
- * structurally — unchanged subtrees keep the SAME reference, and any
2714
- * descendant change yields a NEW root reference. This lets Angular's
2715
- * `Object.is` equality check both detect changes (root reference differs)
2716
- * and short-circuit unchanged subtrees (per-node references stable).
2717
- *
2718
- * Override per-node-type renderers via the `[viewRegistry]` input or by
2719
- * supplying a different `MARKDOWN_VIEW_REGISTRY` provider in the injector
2720
- * tree.
2721
- */
2722
- class ChatStreamingMdComponent {
2723
- content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
2724
- streaming = input(false, ...(ngDevMode ? [{ debugName: "streaming" }] : []));
2725
- viewRegistry = input(undefined, ...(ngDevMode ? [{ debugName: "viewRegistry" }] : []));
2726
- resolvedRegistry = computed(() => this.viewRegistry() ?? cacheplaneMarkdownViews, ...(ngDevMode ? [{ debugName: "resolvedRegistry" }] : []));
2727
- resolver = inject(CitationsResolverService, { optional: true });
2916
+ class ChatConnectedOverlayDirective {
2917
+ origin = input.required({ ...(ngDevMode ? { debugName: "origin" } : {}), alias: 'chatOverlayOrigin' });
2918
+ open = input(false, { ...(ngDevMode ? { debugName: "open" } : {}), alias: 'chatOverlayOpen' });
2919
+ positions = input([], { ...(ngDevMode ? { debugName: "positions" } : {}), alias: 'chatOverlayPositions' });
2920
+ panelClass = input('', { ...(ngDevMode ? { debugName: "panelClass" } : {}), alias: 'chatOverlayPanelClass' });
2921
+ /** Emits the pane element once attached (consumers focus content from it). */
2922
+ attached = output({ alias: 'chatOverlayAttached' });
2923
+ outsideClick = output({ alias: 'chatOverlayOutsideClick' });
2924
+ detached = output({ alias: 'chatOverlayDetach' });
2925
+ templateRef = inject((TemplateRef));
2926
+ viewContainerRef = inject(ViewContainerRef);
2927
+ document = inject(DOCUMENT);
2928
+ pane = null;
2929
+ viewRef = null;
2930
+ resizeObs = null;
2931
+ rafId = 0;
2932
+ previouslyFocused = null;
2933
+ onScrollOrResize = () => this.scheduleReposition();
2934
+ onDocMouseDown = (e) => {
2935
+ if (!this.pane)
2936
+ return;
2937
+ const path = e.composedPath();
2938
+ if (path.includes(this.pane) || path.includes(this.origin().elementRef.nativeElement))
2939
+ return;
2940
+ this.outsideClick.emit(e);
2941
+ };
2942
+ onKeydown = (e) => {
2943
+ if (e.key !== 'Tab' || !this.pane)
2944
+ return;
2945
+ const active = this.document.activeElement;
2946
+ if (this.pane.contains(active) || active === this.origin().elementRef.nativeElement) {
2947
+ this.detached.emit();
2948
+ }
2949
+ };
2728
2950
  constructor() {
2729
2951
  effect(() => {
2730
- const r = this.root();
2731
- if (this.resolver && r) {
2732
- this.resolver.markdownDefs.set(r.citations ?? new Map());
2733
- }
2734
- });
2735
- // Debounced finalization. `finish()` is terminal and DESTRUCTIVE: it reverts
2736
- // an incomplete trailing construct to its CommonMark fallback — e.g. a table
2737
- // header before its delimiter row becomes raw "| a | b |" paragraph text. We
2738
- // must therefore never finalize while tokens are still arriving. The
2739
- // `streaming` input is not a reliable "still arriving" signal — it can flap
2740
- // false mid-stream, and at cold start it can read false for an entire live
2741
- // stream — so we finalize only once streaming is false AND no new content
2742
- // has arrived for a short, imperceptible window. Any new content or a
2743
- // streaming=true flap re-arms the timer, so finalize fires exactly once,
2744
- // after the stream truly stops. Until then the live `parser.root` projection
2745
- // (0.5.x) renders the in-progress content, including streaming tables.
2746
- let timer = null;
2747
- effect((onCleanup) => {
2748
- const isStreaming = this.streaming();
2749
- this.content(); // re-arm whenever new content arrives
2750
- if (timer) {
2751
- clearTimeout(timer);
2752
- timer = null;
2753
- }
2754
- if (isStreaming || this.finished)
2755
- return;
2756
- timer = setTimeout(() => {
2757
- timer = null;
2758
- if (this.streaming() || this.finished)
2759
- return;
2760
- if (!this.prior.endsWith('\n'))
2761
- this.parser.push('\n');
2762
- this.parser.finish();
2763
- this.finished = true;
2764
- this.finalizeTick.update((v) => v + 1);
2765
- }, FINALIZE_DEBOUNCE_MS);
2766
- onCleanup(() => {
2767
- if (timer) {
2768
- clearTimeout(timer);
2769
- timer = null;
2770
- }
2771
- });
2952
+ if (this.open())
2953
+ this.attach();
2954
+ else
2955
+ this.dispose();
2772
2956
  });
2957
+ inject(DestroyRef).onDestroy(() => this.dispose());
2773
2958
  }
2774
- // Parser instance is rebuilt only when content diverges from the prior
2775
- // prefix (rare). For the common streaming case where content extends the
2776
- // prior content, we push the delta and reuse the existing parser tree.
2777
- parser = createPartialMarkdownParser();
2778
- prior = '';
2779
- finished = false;
2780
- // Bumped by the debounced finalizer so the `root` computed re-materializes
2781
- // the now-finished parser tree.
2782
- finalizeTick = signal(0, ...(ngDevMode ? [{ debugName: "finalizeTick" }] : []));
2783
- root = computed(() => {
2784
- const c = this.content();
2785
- this.finalizeTick(); // re-materialize after a debounced finalize
2786
- if (c !== this.prior) {
2787
- // Re-parse from scratch when the content diverged from the prior prefix,
2788
- // OR when the parser was already finalized — finish() is terminal, so
2789
- // pushing further deltas into a finished parser corrupts its state. A
2790
- // transient `streaming=false` mid-stream that finalized early thus
2791
- // recovers here: new content rebuilds an open, projecting parser.
2792
- if (c.startsWith(this.prior) && !this.finished) {
2793
- this.parser.push(c.slice(this.prior.length));
2794
- }
2795
- else {
2796
- this.parser = createPartialMarkdownParser();
2797
- this.finished = false;
2798
- if (c.length > 0)
2799
- this.parser.push(c);
2800
- }
2801
- this.prior = c;
2959
+ attach() {
2960
+ if (this.pane)
2961
+ return;
2962
+ const win = this.document.defaultView;
2963
+ if (!win)
2964
+ return; // SSR / detached document
2965
+ this.previouslyFocused = this.document.activeElement;
2966
+ const pane = this.document.createElement('div');
2967
+ pane.className = 'chat-overlay-pane';
2968
+ for (const c of this.normalizePanelClass())
2969
+ pane.classList.add(c);
2970
+ getOverlayContainer(this.document).appendChild(pane);
2971
+ this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);
2972
+ this.viewRef.detectChanges();
2973
+ for (const node of this.viewRef.rootNodes)
2974
+ pane.appendChild(node);
2975
+ this.pane = pane;
2976
+ this.reposition();
2977
+ win.addEventListener('scroll', this.onScrollOrResize, { capture: true, passive: true });
2978
+ win.addEventListener('resize', this.onScrollOrResize, { passive: true });
2979
+ this.document.addEventListener('mousedown', this.onDocMouseDown, true);
2980
+ this.document.addEventListener('keydown', this.onKeydown, true);
2981
+ if (typeof win.ResizeObserver === 'function') {
2982
+ this.resizeObs = new win.ResizeObserver(() => this.scheduleReposition());
2983
+ this.resizeObs.observe(this.origin().elementRef.nativeElement);
2984
+ this.resizeObs.observe(pane);
2802
2985
  }
2803
- // Materialize for Angular reactivity: produces a NEW root reference when
2804
- // any descendant subtree changed; same reference when nothing changed
2805
- // (structural sharing). This is what makes signal-based CD propagate
2806
- // downstream changes despite the parser preserving identity.
2807
- return materialize(this.parser.root);
2808
- }, ...(ngDevMode ? [{ debugName: "root" }] : []));
2809
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2810
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatStreamingMdComponent, isStandalone: true, selector: "chat-streaming-md", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, viewRegistry: { classPropertyName: "viewRegistry", publicName: "viewRegistry", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2811
- {
2812
- provide: MARKDOWN_VIEW_REGISTRY,
2813
- useFactory: (host) => host.resolvedRegistry(),
2814
- deps: [ChatStreamingMdComponent],
2815
- },
2816
- ], ngImport: i0, template: `
2817
- @if (root(); as r) {
2818
- <chat-md-children [parent]="r" />
2986
+ this.attached.emit(pane);
2819
2987
  }
2820
- `, isInline: true, styles: ["chat-streaming-md{display:block;color:var(--tplane-chat-text);line-height:var(--tplane-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--tplane-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--tplane-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--tplane-chat-text-muted)}chat-streaming-md mark{background:var(--tplane-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--tplane-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--tplane-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--tplane-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:12px 14px;border-radius:var(--tplane-chat-radius-card);overflow-x:auto;font-family:var(--tplane-chat-font-mono);font-size:var(--tplane-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--tplane-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--tplane-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--tplane-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--tplane-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--tplane-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--tplane-chat-surface-alt);border:1px dashed var(--tplane-chat-separator);border-radius:6px;font-size:.9em;color:var(--tplane-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--tplane-chat-font-mono, ui-monospace, monospace)}\n"], dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2821
- }
2822
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, decorators: [{
2823
- type: Component,
2824
- args: [{ selector: 'chat-streaming-md', standalone: true, imports: [MarkdownChildrenComponent], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2825
- @if (root(); as r) {
2826
- <chat-md-children [parent]="r" />
2988
+ scheduleReposition() {
2989
+ const win = this.document.defaultView;
2990
+ if (!win || !this.pane)
2991
+ return;
2992
+ if (this.rafId)
2993
+ win.cancelAnimationFrame(this.rafId);
2994
+ this.rafId = win.requestAnimationFrame(() => this.reposition());
2827
2995
  }
2828
- `, providers: [
2829
- {
2830
- provide: MARKDOWN_VIEW_REGISTRY,
2831
- useFactory: (host) => host.resolvedRegistry(),
2832
- deps: [ChatStreamingMdComponent],
2833
- },
2834
- ], styles: ["chat-streaming-md{display:block;color:var(--tplane-chat-text);line-height:var(--tplane-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--tplane-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--tplane-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--tplane-chat-text-muted)}chat-streaming-md mark{background:var(--tplane-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--tplane-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--tplane-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--tplane-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:12px 14px;border-radius:var(--tplane-chat-radius-card);overflow-x:auto;font-family:var(--tplane-chat-font-mono);font-size:var(--tplane-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--tplane-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--tplane-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--tplane-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--tplane-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--tplane-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--tplane-chat-surface-alt);border:1px dashed var(--tplane-chat-separator);border-radius:6px;font-size:.9em;color:var(--tplane-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--tplane-chat-font-mono, ui-monospace, monospace)}\n"] }]
2835
- }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], viewRegistry: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewRegistry", required: false }] }] } });
2836
-
2837
- // SPDX-License-Identifier: MIT
2838
- /**
2839
- * Render a millisecond duration as a human-readable label suitable for
2840
- * the chat-reasoning "Thought for Ns" pill.
2841
- *
2842
- * - <1 s → "<1s"
2843
- * - 1–59 s → "Ns" (e.g. "4s")
2844
- * - ≥60 s "Nm Ms" (e.g. "1m 12s", "60m 0s")
2845
- *
2846
- * Negative or non-finite inputs collapse to "<1s" so a corrupted timing
2847
- * map never produces noisy output.
2848
- */
2849
- function formatDuration(ms) {
2850
- if (!Number.isFinite(ms) || ms < 1000)
2851
- return '<1s';
2852
- const totalSeconds = Math.floor(ms / 1000);
2853
- if (totalSeconds < 60)
2854
- return `${totalSeconds}s`;
2855
- const minutes = Math.floor(totalSeconds / 60);
2856
- const seconds = totalSeconds - minutes * 60;
2857
- return `${minutes}m ${seconds}s`;
2858
- }
2996
+ reposition() {
2997
+ const win = this.document.defaultView;
2998
+ if (!win || !this.pane)
2999
+ return;
3000
+ const r = this.pane.getBoundingClientRect();
3001
+ const result = computeConnectedPosition({
3002
+ originRect: this.origin().elementRef.nativeElement.getBoundingClientRect(),
3003
+ overlaySize: { width: r.width, height: r.height },
3004
+ viewport: narrowViewport(win, VIEWPORT_MARGIN),
3005
+ positions: this.positions(),
3006
+ });
3007
+ this.pane.style.top = `${Math.round(result.top)}px`;
3008
+ this.pane.style.left = `${Math.round(result.left)}px`;
3009
+ }
3010
+ dispose() {
3011
+ const win = this.document.defaultView;
3012
+ if (this.rafId && win)
3013
+ win.cancelAnimationFrame(this.rafId);
3014
+ this.rafId = 0;
3015
+ if (win) {
3016
+ win.removeEventListener('scroll', this.onScrollOrResize, { capture: true });
3017
+ win.removeEventListener('resize', this.onScrollOrResize);
3018
+ }
3019
+ this.document.removeEventListener('mousedown', this.onDocMouseDown, true);
3020
+ this.document.removeEventListener('keydown', this.onKeydown, true);
3021
+ this.resizeObs?.disconnect();
3022
+ this.resizeObs = null;
3023
+ const focusWasInPane = !!this.pane && this.pane.contains(this.document.activeElement);
3024
+ this.viewRef?.destroy();
3025
+ this.viewRef = null;
3026
+ this.pane?.remove();
3027
+ this.pane = null;
3028
+ if (focusWasInPane && this.previouslyFocused)
3029
+ this.previouslyFocused.focus();
3030
+ this.previouslyFocused = null;
3031
+ }
3032
+ normalizePanelClass() {
3033
+ const pc = this.panelClass();
3034
+ return Array.isArray(pc) ? pc : pc ? [pc] : [];
3035
+ }
3036
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatConnectedOverlayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3037
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.6", type: ChatConnectedOverlayDirective, isStandalone: true, selector: "[chatConnectedOverlay]", inputs: { origin: { classPropertyName: "origin", publicName: "chatOverlayOrigin", isSignal: true, isRequired: true, transformFunction: null }, open: { classPropertyName: "open", publicName: "chatOverlayOpen", isSignal: true, isRequired: false, transformFunction: null }, positions: { classPropertyName: "positions", publicName: "chatOverlayPositions", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "chatOverlayPanelClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { attached: "chatOverlayAttached", outsideClick: "chatOverlayOutsideClick", detached: "chatOverlayDetach" }, ngImport: i0 });
3038
+ }
3039
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatConnectedOverlayDirective, decorators: [{
3040
+ type: Directive,
3041
+ args: [{
3042
+ selector: '[chatConnectedOverlay]',
3043
+ standalone: true,
3044
+ }]
3045
+ }], ctorParameters: () => [], propDecorators: { origin: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayOrigin", required: true }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayOpen", required: false }] }], positions: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayPositions", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayPanelClass", required: false }] }], attached: [{ type: i0.Output, args: ["chatOverlayAttached"] }], outsideClick: [{ type: i0.Output, args: ["chatOverlayOutsideClick"] }], detached: [{ type: i0.Output, args: ["chatOverlayDetach"] }] } });
2859
3046
 
2860
- // libs/chat/src/lib/primitives/chat-reasoning/chat-reasoning.component.ts
3047
+ // libs/chat/src/lib/primitives/chat-citations/chat-citation-preview.component.ts
2861
3048
  // SPDX-License-Identifier: MIT
2862
3049
  /**
2863
- * Renders an assistant's reasoning content as a compact pill that
2864
- * expands to reveal the underlying text. Three visual states:
2865
- *
2866
- * - Streaming: pill shows "Thinking…" with a pulsing dot; auto-expanded
2867
- * so the user sees reasoning stream in real time.
2868
- * - Idle, with durationMs known: pill shows "Thought for {duration}";
2869
- * collapsed by default, expand on click.
2870
- * - Idle, no duration: pill shows "Show reasoning"; collapsed by default.
2871
- *
2872
- * The body re-uses chat-streaming-md so reasoning content gets the same
2873
- * markdown rendering pipeline as the visible response (lists, code,
2874
- * step labels often appear in reasoning output).
2875
- *
2876
- * Internal state: a tristate "expanded" — null means follow auto state-
2877
- * driven logic (force-expand on isStreaming, otherwise honor
2878
- * defaultExpanded), boolean is a manual user choice that wins for the
2879
- * lifetime of the instance.
3050
+ * Presentational provenance card for a single Citation. Rendered inside the
3051
+ * inline marker's connected-overlay pane (hover/tap preview) self-contained
3052
+ * so its encapsulated styles apply even when portaled to the body-level pane.
2880
3053
  */
2881
- class ChatReasoningComponent {
2882
- content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
2883
- isStreaming = input(false, ...(ngDevMode ? [{ debugName: "isStreaming" }] : []));
2884
- durationMs = input(undefined, ...(ngDevMode ? [{ debugName: "durationMs" }] : []));
2885
- label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
2886
- defaultExpanded = input(false, ...(ngDevMode ? [{ debugName: "defaultExpanded" }] : []));
2887
- hasContent = computed(() => (this.content() ?? '').length > 0, ...(ngDevMode ? [{ debugName: "hasContent" }] : []));
2888
- /** null = follow auto logic (streaming expanded, else defaultExpanded). */
2889
- _expandedOverride = signal(null, ...(ngDevMode ? [{ debugName: "_expandedOverride" }] : []));
2890
- expanded = computed(() => {
2891
- const override = this._expandedOverride();
2892
- if (override !== null)
2893
- return override;
2894
- if (this.isStreaming())
2895
- return true;
2896
- return this.defaultExpanded();
2897
- }, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
2898
- expandedStr = computed(() => String(this.expanded()), ...(ngDevMode ? [{ debugName: "expandedStr" }] : []));
2899
- resolvedLabel = computed(() => {
2900
- const explicit = this.label();
2901
- if (explicit)
2902
- return explicit;
2903
- if (this.isStreaming())
2904
- return 'Thinking…';
2905
- const ms = this.durationMs();
2906
- if (typeof ms === 'number')
2907
- return `Thought for ${formatDuration(ms)}`;
2908
- return 'Show reasoning';
2909
- }, ...(ngDevMode ? [{ debugName: "resolvedLabel" }] : []));
3054
+ class ChatCitationPreviewComponent {
3055
+ citation = input.required(...(ngDevMode ? [{ debugName: "citation" }] : []));
3056
+ domain = computed(() => deriveDomain(this.citation().url), ...(ngDevMode ? [{ debugName: "domain" }] : []));
3057
+ monogram = computed(() => deriveMonogram(this.citation()), ...(ngDevMode ? [{ debugName: "monogram" }] : []));
3058
+ monoColor = computed(() => monogramColor(this.citation()), ...(ngDevMode ? [{ debugName: "monoColor" }] : []));
3059
+ typeLabel = computed(() => citationTypeLabel(this.citation()), ...(ngDevMode ? [{ debugName: "typeLabel" }] : []));
3060
+ published = computed(() => formatPublished(this.citation().publishedAt), ...(ngDevMode ? [{ debugName: "published" }] : []));
3061
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3062
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatCitationPreviewComponent, isStandalone: true, selector: "chat-citation-preview", inputs: { citation: { classPropertyName: "citation", publicName: "citation", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3063
+ <div class="chat-citation-preview" role="group" [attr.aria-label]="'Source ' + citation().index">
3064
+ <div class="chat-citation-preview__head">
3065
+ @if (citation().iconUrl; as icon) {
3066
+ <img class="chat-citation-preview__fav" [src]="icon" alt="" width="16" height="16" />
3067
+ } @else {
3068
+ <span class="chat-citation-preview__fav chat-citation-preview__fav--mono"
3069
+ [style.background]="monoColor()">{{ monogram() }}</span>
3070
+ }
3071
+ @if (domain(); as d) { <span class="chat-citation-preview__domain">{{ d }}</span> }
3072
+ @if (typeLabel(); as t) { <span class="chat-citation-preview__type">{{ t }}</span> }
3073
+ </div>
3074
+ @if (citation().title; as title) {
3075
+ <p class="chat-citation-preview__title">{{ title }}</p>
3076
+ }
3077
+ @if (citation().snippet; as s) {
3078
+ <p class="chat-citation-preview__snippet">{{ s }}</p>
3079
+ }
3080
+ @if (citation().url; as url) {
3081
+ <div class="chat-citation-preview__foot">
3082
+ <a class="chat-citation-preview__open" [href]="url" target="_blank" rel="noopener noreferrer">
3083
+ <svg viewBox="0 0 24 24" aria-hidden="true" width="12" height="12">
3084
+ <path d="M7 17L17 7M17 7H8M17 7v9" fill="none" stroke="currentColor" stroke-width="2" />
3085
+ </svg>
3086
+ Open source
3087
+ </a>
3088
+ @if (published(); as p) { <span class="chat-citation-preview__meta">{{ p }}</span> }
3089
+ </div>
3090
+ }
3091
+ </div>
3092
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citation-preview{width:320px;max-width:calc(100vw - 24px);box-sizing:border-box;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);box-shadow:var(--tplane-chat-shadow-md);padding:12px 13px;text-align:left;color:var(--tplane-chat-text)}.chat-citation-preview__head{display:flex;align-items:center;gap:7px;margin-bottom:7px}.chat-citation-preview__fav{width:16px;height:16px;border-radius:4px;flex:0 0 auto;object-fit:cover}.chat-citation-preview__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:700}.chat-citation-preview__domain{font-size:12px;color:var(--tplane-chat-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citation-preview__type{margin-left:auto;font-size:11px;color:var(--tplane-chat-text-muted);flex:0 0 auto}.chat-citation-preview__title{font-size:14px;font-weight:600;line-height:1.35;margin:0 0 5px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.chat-citation-preview__snippet{font-size:12.5px;color:var(--tplane-chat-text-muted);line-height:1.5;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.chat-citation-preview__foot{display:flex;align-items:center;justify-content:space-between;margin-top:10px;padding-top:9px;border-top:1px solid var(--tplane-chat-separator)}.chat-citation-preview__open{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;color:var(--tplane-chat-citation-accent);text-decoration:none}.chat-citation-preview__open:hover{text-decoration:underline}.chat-citation-preview__meta{font-size:11px;color:var(--tplane-chat-text-muted)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3093
+ }
3094
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatCitationPreviewComponent, decorators: [{
3095
+ type: Component,
3096
+ args: [{ selector: 'chat-citation-preview', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3097
+ <div class="chat-citation-preview" role="group" [attr.aria-label]="'Source ' + citation().index">
3098
+ <div class="chat-citation-preview__head">
3099
+ @if (citation().iconUrl; as icon) {
3100
+ <img class="chat-citation-preview__fav" [src]="icon" alt="" width="16" height="16" />
3101
+ } @else {
3102
+ <span class="chat-citation-preview__fav chat-citation-preview__fav--mono"
3103
+ [style.background]="monoColor()">{{ monogram() }}</span>
3104
+ }
3105
+ @if (domain(); as d) { <span class="chat-citation-preview__domain">{{ d }}</span> }
3106
+ @if (typeLabel(); as t) { <span class="chat-citation-preview__type">{{ t }}</span> }
3107
+ </div>
3108
+ @if (citation().title; as title) {
3109
+ <p class="chat-citation-preview__title">{{ title }}</p>
3110
+ }
3111
+ @if (citation().snippet; as s) {
3112
+ <p class="chat-citation-preview__snippet">{{ s }}</p>
3113
+ }
3114
+ @if (citation().url; as url) {
3115
+ <div class="chat-citation-preview__foot">
3116
+ <a class="chat-citation-preview__open" [href]="url" target="_blank" rel="noopener noreferrer">
3117
+ <svg viewBox="0 0 24 24" aria-hidden="true" width="12" height="12">
3118
+ <path d="M7 17L17 7M17 7H8M17 7v9" fill="none" stroke="currentColor" stroke-width="2" />
3119
+ </svg>
3120
+ Open source
3121
+ </a>
3122
+ @if (published(); as p) { <span class="chat-citation-preview__meta">{{ p }}</span> }
3123
+ </div>
3124
+ }
3125
+ </div>
3126
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-citation-preview{width:320px;max-width:calc(100vw - 24px);box-sizing:border-box;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:var(--tplane-chat-radius-card);box-shadow:var(--tplane-chat-shadow-md);padding:12px 13px;text-align:left;color:var(--tplane-chat-text)}.chat-citation-preview__head{display:flex;align-items:center;gap:7px;margin-bottom:7px}.chat-citation-preview__fav{width:16px;height:16px;border-radius:4px;flex:0 0 auto;object-fit:cover}.chat-citation-preview__fav--mono{display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:700}.chat-citation-preview__domain{font-size:12px;color:var(--tplane-chat-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-citation-preview__type{margin-left:auto;font-size:11px;color:var(--tplane-chat-text-muted);flex:0 0 auto}.chat-citation-preview__title{font-size:14px;font-weight:600;line-height:1.35;margin:0 0 5px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.chat-citation-preview__snippet{font-size:12.5px;color:var(--tplane-chat-text-muted);line-height:1.5;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.chat-citation-preview__foot{display:flex;align-items:center;justify-content:space-between;margin-top:10px;padding-top:9px;border-top:1px solid var(--tplane-chat-separator)}.chat-citation-preview__open{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;color:var(--tplane-chat-citation-accent);text-decoration:none}.chat-citation-preview__open:hover{text-decoration:underline}.chat-citation-preview__meta{font-size:11px;color:var(--tplane-chat-text-muted)}\n"] }]
3127
+ }], propDecorators: { citation: [{ type: i0.Input, args: [{ isSignal: true, alias: "citation", required: true }] }] } });
3128
+
3129
+ // libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts
3130
+ // SPDX-License-Identifier: MIT
3131
+ const OPEN_DELAY_MS = 120;
3132
+ const CLOSE_DELAY_MS = 200;
3133
+ /**
3134
+ * Inline citation marker. Renders a numbered pill and reveals a provenance
3135
+ * preview card (portaled via the connected-overlay primitive) on hover/focus
3136
+ * (desktop) or tap (touch). Click navigates on desktop when a url exists;
3137
+ * on touch it previews instead of navigating.
3138
+ */
3139
+ class MarkdownCitationReferenceComponent {
3140
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
3141
+ resolver = inject(CitationsResolverService);
3142
+ document = inject(DOCUMENT);
3143
+ resolved = computed(() => this.resolver.lookup(this.node().refId)(), ...(ngDevMode ? [{ debugName: "resolved" }] : []));
3144
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
3145
+ // Prefer below-start, flip above when it won't fit. The positioner clamps to view.
3146
+ positions = [
3147
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 6 },
3148
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -6 },
3149
+ ];
3150
+ /** Read fresh each time: SSR renders with no window, and hybrid devices change. */
3151
+ get hoverCapable() {
3152
+ return this.document.defaultView?.matchMedia?.('(hover: hover) and (pointer: fine)').matches ?? false;
3153
+ }
3154
+ openTimer = 0;
3155
+ closeTimer = 0;
3156
+ pane = null;
3157
+ justOpenedByFocus = false;
2910
3158
  constructor() {
2911
- // Reset the manual override when streaming re-engages from idle (e.g.
2912
- // follow-up turn that re-uses this instance) so the auto force-expand
2913
- // logic takes over again. Spec §3.3 bullet 3.
2914
- let prevStreaming = false;
2915
- effect(() => {
2916
- const streaming = this.isStreaming();
2917
- if (!prevStreaming && streaming) {
2918
- this._expandedOverride.set(null);
2919
- }
2920
- prevStreaming = streaming;
2921
- });
3159
+ inject(DestroyRef).onDestroy(() => this.clearTimers());
3160
+ }
3161
+ ariaLabel(c) {
3162
+ const domain = deriveDomain(c.url);
3163
+ const parts = [`Source ${c.index}`];
3164
+ if (c.title)
3165
+ parts.push(c.title);
3166
+ if (domain)
3167
+ parts.push(domain);
3168
+ const base = parts.join(', ');
3169
+ return c.url ? `${base}, opens in new tab` : base;
3170
+ }
3171
+ onEnter() {
3172
+ if (!this.hoverCapable)
3173
+ return;
3174
+ this.cancelClose();
3175
+ const win = this.document.defaultView;
3176
+ if (win)
3177
+ this.openTimer = win.setTimeout(() => this.open.set(true), OPEN_DELAY_MS);
2922
3178
  }
2923
- toggle() {
2924
- this._expandedOverride.set(!this.expanded());
3179
+ onLeave() {
3180
+ if (!this.hoverCapable)
3181
+ return;
3182
+ this.cancelOpen();
3183
+ this.scheduleClose();
2925
3184
  }
2926
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatReasoningComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2927
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatReasoningComponent, isStandalone: true, selector: "chat-reasoning", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, isStreaming: { classPropertyName: "isStreaming", publicName: "isStreaming", isSignal: true, isRequired: false, transformFunction: null }, durationMs: { classPropertyName: "durationMs", publicName: "durationMs", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, defaultExpanded: { classPropertyName: "defaultExpanded", publicName: "defaultExpanded", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-has-content": "hasContent()", "attr.data-expanded": "expandedStr()", "attr.data-streaming": "isStreaming()" } }, ngImport: i0, template: `
2928
- <button
2929
- type="button"
2930
- class="chat-reasoning__header"
2931
- [attr.aria-expanded]="expanded()"
2932
- (click)="toggle()"
2933
- >
2934
- <svg class="chat-reasoning__chevron" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2935
- <path d="M4 2l4 4-4 4"/>
2936
- </svg>
2937
- @if (isStreaming()) {
2938
- <span class="chat-reasoning__pulse" aria-hidden="true"></span>
2939
- }
2940
- <span class="chat-reasoning__label">{{ resolvedLabel() }}</span>
2941
- </button>
2942
- @if (expanded()) {
2943
- <div class="chat-reasoning__body">
2944
- <chat-streaming-md [content]="content()" [streaming]="isStreaming()" />
2945
- </div>
3185
+ onFocus() {
3186
+ this.open.set(true);
3187
+ this.justOpenedByFocus = true;
2946
3188
  }
2947
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;margin:0 0 .5rem}:host([data-has-content=\"false\"]){display:none}.chat-reasoning__header{display:inline-flex;align-items:center;gap:.5rem;padding:4px 10px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:9999px;color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-xs);font-family:inherit;cursor:pointer;line-height:1.2}.chat-reasoning__header:hover{color:var(--tplane-chat-text)}.chat-reasoning__chevron{width:10px;height:10px;transition:transform .12s ease}:host([data-expanded=\"true\"]) .chat-reasoning__chevron{transform:rotate(90deg)}.chat-reasoning__pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:chat-reasoning-pulse 1.2s ease-in-out infinite}@keyframes chat-reasoning-pulse{0%,to{opacity:.3}50%{opacity:1}}.chat-reasoning__body{margin-top:.5rem;padding-left:12px;border-left:2px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted)}.chat-reasoning__body chat-streaming-md{font-size:.95em}\n"], dependencies: [{ kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3189
+ onBlur() {
3190
+ this.justOpenedByFocus = false;
3191
+ const active = this.document.activeElement;
3192
+ if (this.pane && active && this.pane.contains(active))
3193
+ return; // focus moved into card
3194
+ this.close();
3195
+ }
3196
+ onClick(e, c) {
3197
+ // Desktop + real url: let the native link navigate.
3198
+ if (this.hoverCapable && c.url)
3199
+ return;
3200
+ // No url, or touch device: preview instead of navigating.
3201
+ e.preventDefault();
3202
+ if (this.justOpenedByFocus) {
3203
+ // focus (fired first in this tap/gesture) already opened it — don't toggle closed
3204
+ this.justOpenedByFocus = false;
3205
+ return;
3206
+ }
3207
+ this.open.update((v) => !v);
3208
+ }
3209
+ onKeydown(e, c) {
3210
+ if (e.key === 'Escape') {
3211
+ this.close();
3212
+ return;
3213
+ }
3214
+ // A no-url marker is a button: Enter/Space reveals the preview (Escape/blur close it).
3215
+ if (!c.url && (e.key === 'Enter' || e.key === ' ')) {
3216
+ e.preventDefault();
3217
+ this.open.set(true);
3218
+ }
3219
+ }
3220
+ onAttached(pane) {
3221
+ this.pane = pane;
3222
+ pane.addEventListener('mouseenter', this.onPaneEnter);
3223
+ pane.addEventListener('mouseleave', this.onPaneLeave);
3224
+ }
3225
+ close() {
3226
+ this.clearTimers();
3227
+ this.justOpenedByFocus = false;
3228
+ this.open.set(false);
3229
+ this.pane = null; // directive removes the pane element on detach
3230
+ }
3231
+ onPaneEnter = () => this.cancelClose();
3232
+ onPaneLeave = () => this.scheduleClose();
3233
+ scheduleClose() {
3234
+ const win = this.document.defaultView;
3235
+ if (win)
3236
+ this.closeTimer = win.setTimeout(() => this.open.set(false), CLOSE_DELAY_MS);
3237
+ }
3238
+ cancelOpen() {
3239
+ const win = this.document.defaultView;
3240
+ if (this.openTimer && win)
3241
+ win.clearTimeout(this.openTimer);
3242
+ this.openTimer = 0;
3243
+ }
3244
+ cancelClose() {
3245
+ const win = this.document.defaultView;
3246
+ if (this.closeTimer && win)
3247
+ win.clearTimeout(this.closeTimer);
3248
+ this.closeTimer = 0;
3249
+ }
3250
+ clearTimers() {
3251
+ this.cancelOpen();
3252
+ this.cancelClose();
3253
+ }
3254
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownCitationReferenceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3255
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownCitationReferenceComponent, isStandalone: true, selector: "chat-md-citation-reference", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3256
+ @if (resolved(); as r) {
3257
+ <a
3258
+ class="chat-citation-marker"
3259
+ [class.chat-citation-marker--no-url]="!r.citation.url"
3260
+ chatOverlayOrigin
3261
+ #origin="chatOverlayOrigin"
3262
+ [attr.href]="r.citation.url ?? null"
3263
+ [attr.target]="r.citation.url ? '_blank' : null"
3264
+ [attr.rel]="r.citation.url ? 'noopener noreferrer' : null"
3265
+ [attr.role]="r.citation.url ? null : 'button'"
3266
+ [attr.tabindex]="r.citation.url ? null : '0'"
3267
+ aria-haspopup="dialog"
3268
+ [attr.aria-expanded]="open()"
3269
+ [attr.aria-label]="ariaLabel(r.citation)"
3270
+ (mouseenter)="onEnter()"
3271
+ (mouseleave)="onLeave()"
3272
+ (focus)="onFocus()"
3273
+ (blur)="onBlur()"
3274
+ (click)="onClick($event, r.citation)"
3275
+ (keydown)="onKeydown($event, r.citation)"
3276
+ >{{ node().index }}</a>
3277
+ <ng-template
3278
+ chatConnectedOverlay
3279
+ [chatOverlayOrigin]="origin"
3280
+ [chatOverlayOpen]="open()"
3281
+ [chatOverlayPositions]="positions"
3282
+ [chatOverlayPanelClass]="'chat-citation-preview-pane'"
3283
+ (chatOverlayAttached)="onAttached($event)"
3284
+ (chatOverlayOutsideClick)="close()"
3285
+ (chatOverlayDetach)="close()"
3286
+ >
3287
+ <chat-citation-preview [citation]="r.citation" />
3288
+ </ng-template>
3289
+ } @else {
3290
+ <span
3291
+ class="chat-citation-marker chat-citation-marker--unresolved"
3292
+ [attr.title]="'No source available'"
3293
+ [attr.aria-label]="'Citation ' + node().index + ': source unavailable'"
3294
+ >{{ node().index }}</span>
3295
+ }
3296
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline}.chat-citation-marker{display:inline-flex;align-items:center;justify-content:center;min-width:17px;height:17px;padding:0 5px;margin:0 1px;font-size:11px;font-weight:600;line-height:1;color:var(--tplane-chat-citation-marker-fg);background:var(--tplane-chat-citation-marker-bg);border:1px solid var(--tplane-chat-citation-marker-border);border-radius:var(--tplane-chat-citation-radius);translate:0 -1px;text-decoration:none;cursor:pointer;white-space:nowrap;transition:background .12s ease,border-color .12s ease,color .12s ease}.chat-citation-marker:hover,.chat-citation-marker:focus-visible{background:var(--tplane-chat-citation-accent-soft);border-color:var(--tplane-chat-citation-accent-border);color:var(--tplane-chat-citation-accent);outline:none}.chat-citation-marker:focus-visible{box-shadow:0 0 0 2px var(--tplane-chat-citation-accent-border)}.chat-citation-marker--unresolved{color:var(--tplane-chat-text-muted);background:transparent;border-style:dashed;cursor:default}.chat-citation-marker--unresolved:hover{background:transparent;border-color:var(--tplane-chat-citation-marker-border);color:var(--tplane-chat-text-muted)}.chat-citation-marker--no-url{cursor:help}\n"], dependencies: [{ kind: "directive", type: ChatConnectedOverlayDirective, selector: "[chatConnectedOverlay]", inputs: ["chatOverlayOrigin", "chatOverlayOpen", "chatOverlayPositions", "chatOverlayPanelClass"], outputs: ["chatOverlayAttached", "chatOverlayOutsideClick", "chatOverlayDetach"] }, { kind: "directive", type: ChatOverlayOriginDirective, selector: "[chatOverlayOrigin]", exportAs: ["chatOverlayOrigin"] }, { kind: "component", type: ChatCitationPreviewComponent, selector: "chat-citation-preview", inputs: ["citation"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2948
3297
  }
2949
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatReasoningComponent, decorators: [{
3298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownCitationReferenceComponent, decorators: [{
2950
3299
  type: Component,
2951
- args: [{ selector: 'chat-reasoning', standalone: true, imports: [ChatStreamingMdComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2952
- '[attr.data-has-content]': 'hasContent()',
2953
- '[attr.data-expanded]': 'expandedStr()',
2954
- '[attr.data-streaming]': 'isStreaming()',
2955
- }, template: `
2956
- <button
2957
- type="button"
2958
- class="chat-reasoning__header"
2959
- [attr.aria-expanded]="expanded()"
2960
- (click)="toggle()"
2961
- >
2962
- <svg class="chat-reasoning__chevron" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2963
- <path d="M4 2l4 4-4 4"/>
2964
- </svg>
2965
- @if (isStreaming()) {
2966
- <span class="chat-reasoning__pulse" aria-hidden="true"></span>
2967
- }
2968
- <span class="chat-reasoning__label">{{ resolvedLabel() }}</span>
2969
- </button>
2970
- @if (expanded()) {
2971
- <div class="chat-reasoning__body">
2972
- <chat-streaming-md [content]="content()" [streaming]="isStreaming()" />
2973
- </div>
3300
+ args: [{ selector: 'chat-md-citation-reference', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ChatConnectedOverlayDirective, ChatOverlayOriginDirective, ChatCitationPreviewComponent], template: `
3301
+ @if (resolved(); as r) {
3302
+ <a
3303
+ class="chat-citation-marker"
3304
+ [class.chat-citation-marker--no-url]="!r.citation.url"
3305
+ chatOverlayOrigin
3306
+ #origin="chatOverlayOrigin"
3307
+ [attr.href]="r.citation.url ?? null"
3308
+ [attr.target]="r.citation.url ? '_blank' : null"
3309
+ [attr.rel]="r.citation.url ? 'noopener noreferrer' : null"
3310
+ [attr.role]="r.citation.url ? null : 'button'"
3311
+ [attr.tabindex]="r.citation.url ? null : '0'"
3312
+ aria-haspopup="dialog"
3313
+ [attr.aria-expanded]="open()"
3314
+ [attr.aria-label]="ariaLabel(r.citation)"
3315
+ (mouseenter)="onEnter()"
3316
+ (mouseleave)="onLeave()"
3317
+ (focus)="onFocus()"
3318
+ (blur)="onBlur()"
3319
+ (click)="onClick($event, r.citation)"
3320
+ (keydown)="onKeydown($event, r.citation)"
3321
+ >{{ node().index }}</a>
3322
+ <ng-template
3323
+ chatConnectedOverlay
3324
+ [chatOverlayOrigin]="origin"
3325
+ [chatOverlayOpen]="open()"
3326
+ [chatOverlayPositions]="positions"
3327
+ [chatOverlayPanelClass]="'chat-citation-preview-pane'"
3328
+ (chatOverlayAttached)="onAttached($event)"
3329
+ (chatOverlayOutsideClick)="close()"
3330
+ (chatOverlayDetach)="close()"
3331
+ >
3332
+ <chat-citation-preview [citation]="r.citation" />
3333
+ </ng-template>
3334
+ } @else {
3335
+ <span
3336
+ class="chat-citation-marker chat-citation-marker--unresolved"
3337
+ [attr.title]="'No source available'"
3338
+ [attr.aria-label]="'Citation ' + node().index + ': source unavailable'"
3339
+ >{{ node().index }}</span>
2974
3340
  }
2975
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;margin:0 0 .5rem}:host([data-has-content=\"false\"]){display:none}.chat-reasoning__header{display:inline-flex;align-items:center;gap:.5rem;padding:4px 10px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:9999px;color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-xs);font-family:inherit;cursor:pointer;line-height:1.2}.chat-reasoning__header:hover{color:var(--tplane-chat-text)}.chat-reasoning__chevron{width:10px;height:10px;transition:transform .12s ease}:host([data-expanded=\"true\"]) .chat-reasoning__chevron{transform:rotate(90deg)}.chat-reasoning__pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:chat-reasoning-pulse 1.2s ease-in-out infinite}@keyframes chat-reasoning-pulse{0%,to{opacity:.3}50%{opacity:1}}.chat-reasoning__body{margin-top:.5rem;padding-left:12px;border-left:2px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted)}.chat-reasoning__body chat-streaming-md{font-size:.95em}\n"] }]
2976
- }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], isStreaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStreaming", required: false }] }], durationMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "durationMs", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], defaultExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultExpanded", required: false }] }] } });
3341
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline}.chat-citation-marker{display:inline-flex;align-items:center;justify-content:center;min-width:17px;height:17px;padding:0 5px;margin:0 1px;font-size:11px;font-weight:600;line-height:1;color:var(--tplane-chat-citation-marker-fg);background:var(--tplane-chat-citation-marker-bg);border:1px solid var(--tplane-chat-citation-marker-border);border-radius:var(--tplane-chat-citation-radius);translate:0 -1px;text-decoration:none;cursor:pointer;white-space:nowrap;transition:background .12s ease,border-color .12s ease,color .12s ease}.chat-citation-marker:hover,.chat-citation-marker:focus-visible{background:var(--tplane-chat-citation-accent-soft);border-color:var(--tplane-chat-citation-accent-border);color:var(--tplane-chat-citation-accent);outline:none}.chat-citation-marker:focus-visible{box-shadow:0 0 0 2px var(--tplane-chat-citation-accent-border)}.chat-citation-marker--unresolved{color:var(--tplane-chat-text-muted);background:transparent;border-style:dashed;cursor:default}.chat-citation-marker--unresolved:hover{background:transparent;border-color:var(--tplane-chat-citation-marker-border);color:var(--tplane-chat-text-muted)}.chat-citation-marker--no-url{cursor:help}\n"] }]
3342
+ }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2977
3343
 
2978
- // libs/chat/src/lib/styles/chat-launcher-button.styles.ts
3344
+ // libs/chat/src/lib/markdown/views/markdown-table.component.ts
2979
3345
  // SPDX-License-Identifier: MIT
2980
- const CHAT_LAUNCHER_BUTTON_STYLES = `
2981
- :host { display: inline-block; }
2982
- .chat-launcher-button {
2983
- width: 56px;
2984
- height: 56px;
2985
- border-radius: var(--tplane-chat-radius-launcher);
2986
- background: var(--tplane-chat-primary);
2987
- color: var(--tplane-chat-on-primary);
2988
- border: 0;
2989
- cursor: pointer;
2990
- display: flex;
2991
- align-items: center;
2992
- justify-content: center;
2993
- box-shadow: var(--tplane-chat-shadow-md);
2994
- transition: transform 200ms ease;
2995
- }
2996
- .chat-launcher-button:hover { transform: scale(1.05); }
2997
- .chat-launcher-button svg { width: 24px; height: 24px; }
2998
- `;
2999
-
3000
- // SPDX-License-Identifier: MIT
3001
- class ChatLauncherButtonComponent {
3002
- /** Fires when the inner <button> receives a click. Prefer this over
3003
- * binding `(click)` on the host element — explicit output gives
3004
- * consumers (and Playwright) an unambiguous click target that won't
3005
- * be intercepted by sibling overlays in higher stacking contexts.
3006
- * Native `(click)` on the host still works for back-compat: the
3007
- * click event bubbles through unchanged. */
3008
- clicked = output();
3009
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatLauncherButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3010
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.6", type: ChatLauncherButtonComponent, isStandalone: true, selector: "chat-launcher-button", outputs: { clicked: "clicked" }, ngImport: i0, template: `
3011
- <button type="button" class="chat-launcher-button" aria-label="Open chat" (click)="clicked.emit()">
3012
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3013
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3014
- </svg>
3015
- </button>
3016
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline-block}.chat-launcher-button{width:56px;height:56px;border-radius:var(--tplane-chat-radius-launcher);background:var(--tplane-chat-primary);color:var(--tplane-chat-on-primary);border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--tplane-chat-shadow-md);transition:transform .2s ease}.chat-launcher-button:hover{transform:scale(1.05)}.chat-launcher-button svg{width:24px;height:24px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3346
+ class MarkdownTableComponent {
3347
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
3348
+ headerRow = computed(() => {
3349
+ const rows = this.node().children;
3350
+ return rows.length > 0 && rows[0].isHeader ? rows[0] : null;
3351
+ }, ...(ngDevMode ? [{ debugName: "headerRow" }] : []));
3352
+ bodyRows = computed(() => {
3353
+ const rows = this.node().children;
3354
+ return rows[0]?.isHeader ? rows.slice(1) : rows;
3355
+ }, ...(ngDevMode ? [{ debugName: "bodyRows" }] : []));
3356
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3357
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableComponent, isStandalone: true, selector: "chat-md-table", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3358
+ <table class="chat-md-table">
3359
+ <thead>
3360
+ @if (headerRow(); as row) {
3361
+ <tr class="chat-md-table-row chat-md-table-row--header">
3362
+ @for (cell of row.children; track $index) {
3363
+ <th class="chat-md-table-cell" [style.text-align]="cell.alignment ?? null">
3364
+ <chat-md-children [parent]="cell" />
3365
+ </th>
3366
+ }
3367
+ </tr>
3368
+ }
3369
+ </thead>
3370
+ <tbody>
3371
+ @for (row of bodyRows(); track $index) {
3372
+ <tr class="chat-md-table-row">
3373
+ @for (cell of row.children; track $index) {
3374
+ <td class="chat-md-table-cell" [style.text-align]="cell.alignment ?? null">
3375
+ <chat-md-children [parent]="cell" />
3376
+ </td>
3377
+ }
3378
+ </tr>
3379
+ }
3380
+ </tbody>
3381
+ </table>
3382
+ `, isInline: true, dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3017
3383
  }
3018
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatLauncherButtonComponent, decorators: [{
3384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableComponent, decorators: [{
3019
3385
  type: Component,
3020
- args: [{ selector: 'chat-launcher-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3021
- <button type="button" class="chat-launcher-button" aria-label="Open chat" (click)="clicked.emit()">
3022
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3023
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3024
- </svg>
3025
- </button>
3026
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline-block}.chat-launcher-button{width:56px;height:56px;border-radius:var(--tplane-chat-radius-launcher);background:var(--tplane-chat-primary);color:var(--tplane-chat-on-primary);border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--tplane-chat-shadow-md);transition:transform .2s ease}.chat-launcher-button:hover{transform:scale(1.05)}.chat-launcher-button svg{width:24px;height:24px}\n"] }]
3027
- }], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
3386
+ args: [{
3387
+ selector: 'chat-md-table',
3388
+ standalone: true,
3389
+ imports: [MarkdownChildrenComponent],
3390
+ changeDetection: ChangeDetectionStrategy.OnPush,
3391
+ template: `
3392
+ <table class="chat-md-table">
3393
+ <thead>
3394
+ @if (headerRow(); as row) {
3395
+ <tr class="chat-md-table-row chat-md-table-row--header">
3396
+ @for (cell of row.children; track $index) {
3397
+ <th class="chat-md-table-cell" [style.text-align]="cell.alignment ?? null">
3398
+ <chat-md-children [parent]="cell" />
3399
+ </th>
3400
+ }
3401
+ </tr>
3402
+ }
3403
+ </thead>
3404
+ <tbody>
3405
+ @for (row of bodyRows(); track $index) {
3406
+ <tr class="chat-md-table-row">
3407
+ @for (cell of row.children; track $index) {
3408
+ <td class="chat-md-table-cell" [style.text-align]="cell.alignment ?? null">
3409
+ <chat-md-children [parent]="cell" />
3410
+ </td>
3411
+ }
3412
+ </tr>
3413
+ }
3414
+ </tbody>
3415
+ </table>
3416
+ `,
3417
+ }]
3418
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
3028
3419
 
3420
+ // libs/chat/src/lib/markdown/markdown-table-row.token.ts
3029
3421
  // SPDX-License-Identifier: MIT
3030
- const CHAT_SUGGESTIONS_STYLES = `
3031
- :host { display: block; }
3032
- .chat-suggestions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
3033
- .chat-suggestion {
3034
- padding: 6px 10px;
3035
- font-size: var(--tplane-chat-font-size-xs);
3036
- border-radius: var(--tplane-chat-radius-bubble);
3037
- border: 1px solid var(--tplane-chat-muted);
3038
- background: transparent;
3039
- color: var(--tplane-chat-text);
3040
- cursor: pointer;
3041
- transition: transform 200ms ease;
3042
- }
3043
- .chat-suggestion:hover { transform: scale(1.03); }
3044
- .chat-suggestion:disabled { cursor: wait; opacity: 0.6; }
3045
- `;
3422
+ /**
3423
+ * Provided by MarkdownTableRowComponent for header rows so that
3424
+ * MarkdownTableCellComponent can render <th> instead of <td>.
3425
+ * The value is a Signal<boolean> so that it tracks the row's isHeader reactively.
3426
+ */
3427
+ const IS_HEADER_ROW = new InjectionToken('IS_HEADER_ROW', {
3428
+ providedIn: null,
3429
+ factory: () => signal(false),
3430
+ });
3046
3431
 
3432
+ // libs/chat/src/lib/markdown/views/markdown-table-row.component.ts
3047
3433
  // SPDX-License-Identifier: MIT
3048
- class ChatSuggestionsComponent {
3049
- suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
3050
- selected = output();
3051
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3052
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatSuggestionsComponent, isStandalone: true, selector: "chat-suggestions", inputs: { suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: `
3053
- <div class="chat-suggestions">
3054
- @for (s of suggestions(); track s) {
3055
- <button type="button" class="chat-suggestion" (click)="selected.emit(s)">{{ s }}</button>
3434
+ class MarkdownTableRowComponent {
3435
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
3436
+ registry = inject(MARKDOWN_VIEW_REGISTRY);
3437
+ resolve(child) {
3438
+ const entry = this.registry[child.type];
3439
+ if (!entry)
3440
+ return null;
3441
+ return typeof entry === 'function' ? entry : entry.component;
3442
+ }
3443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3444
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableRowComponent, isStandalone: true, selector: "chat-md-table-row", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
3445
+ {
3446
+ provide: IS_HEADER_ROW,
3447
+ useFactory: () => {
3448
+ const comp = inject(MarkdownTableRowComponent);
3449
+ return computed(() => comp.node().isHeader);
3450
+ },
3451
+ },
3452
+ ], ngImport: i0, template: `
3453
+ <tr class="chat-md-table-row" [class.chat-md-table-row--header]="node().isHeader">
3454
+ @for (child of node().children; track $index) {
3455
+ @let comp = resolve(child);
3456
+ @if (comp) {
3457
+ <ng-container *ngComponentOutlet="comp; inputs: { node: child }" />
3458
+ }
3056
3459
  }
3057
- </div>
3058
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}.chat-suggestion{padding:6px 10px;font-size:var(--tplane-chat-font-size-xs);border-radius:var(--tplane-chat-radius-bubble);border:1px solid var(--tplane-chat-muted);background:transparent;color:var(--tplane-chat-text);cursor:pointer;transition:transform .2s ease}.chat-suggestion:hover{transform:scale(1.03)}.chat-suggestion:disabled{cursor:wait;opacity:.6}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3460
+ </tr>
3461
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3059
3462
  }
3060
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSuggestionsComponent, decorators: [{
3463
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableRowComponent, decorators: [{
3061
3464
  type: Component,
3062
- args: [{ selector: 'chat-suggestions', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3063
- <div class="chat-suggestions">
3064
- @for (s of suggestions(); track s) {
3065
- <button type="button" class="chat-suggestion" (click)="selected.emit(s)">{{ s }}</button>
3465
+ args: [{
3466
+ selector: 'chat-md-table-row',
3467
+ standalone: true,
3468
+ imports: [NgComponentOutlet],
3469
+ changeDetection: ChangeDetectionStrategy.OnPush,
3470
+ template: `
3471
+ <tr class="chat-md-table-row" [class.chat-md-table-row--header]="node().isHeader">
3472
+ @for (child of node().children; track $index) {
3473
+ @let comp = resolve(child);
3474
+ @if (comp) {
3475
+ <ng-container *ngComponentOutlet="comp; inputs: { node: child }" />
3476
+ }
3066
3477
  }
3067
- </div>
3068
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}.chat-suggestion{padding:6px 10px;font-size:var(--tplane-chat-font-size-xs);border-radius:var(--tplane-chat-radius-bubble);border:1px solid var(--tplane-chat-muted);background:transparent;color:var(--tplane-chat-text);cursor:pointer;transition:transform .2s ease}.chat-suggestion:hover{transform:scale(1.03)}.chat-suggestion:disabled{cursor:wait;opacity:.6}\n"] }]
3069
- }], propDecorators: { suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
3478
+ </tr>
3479
+ `,
3480
+ providers: [
3481
+ {
3482
+ provide: IS_HEADER_ROW,
3483
+ useFactory: () => {
3484
+ const comp = inject(MarkdownTableRowComponent);
3485
+ return computed(() => comp.node().isHeader);
3486
+ },
3487
+ },
3488
+ ],
3489
+ }]
3490
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
3070
3491
 
3071
- // libs/chat/src/lib/styles/chat-input.styles.ts
3492
+ // libs/chat/src/lib/markdown/views/markdown-table-cell.component.ts
3072
3493
  // SPDX-License-Identifier: MIT
3073
- const CHAT_INPUT_STYLES = `
3074
- :host {
3075
- display: block;
3076
- width: 100%;
3077
- padding: 0 var(--tplane-chat-edge-pad);
3078
- box-sizing: border-box;
3494
+ class MarkdownTableCellComponent {
3495
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
3496
+ isHeaderRowToken = inject(IS_HEADER_ROW, { optional: true });
3497
+ isHeader = computed(() => this.isHeaderRowToken ? this.isHeaderRowToken() : false, ...(ngDevMode ? [{ debugName: "isHeader" }] : []));
3498
+ alignment = computed(() => this.node().alignment, ...(ngDevMode ? [{ debugName: "alignment" }] : []));
3499
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3500
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownTableCellComponent, isStandalone: true, selector: "chat-md-table-cell", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3501
+ @if (isHeader()) {
3502
+ <th class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
3503
+ <chat-md-children [parent]="node()" />
3504
+ </th>
3505
+ } @else {
3506
+ <td class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
3507
+ <chat-md-children [parent]="node()" />
3508
+ </td>
3509
+ }
3510
+ `, isInline: true, dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3079
3511
  }
3512
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownTableCellComponent, decorators: [{
3513
+ type: Component,
3514
+ args: [{
3515
+ selector: 'chat-md-table-cell',
3516
+ standalone: true,
3517
+ imports: [MarkdownChildrenComponent],
3518
+ changeDetection: ChangeDetectionStrategy.OnPush,
3519
+ template: `
3520
+ @if (isHeader()) {
3521
+ <th class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
3522
+ <chat-md-children [parent]="node()" />
3523
+ </th>
3524
+ } @else {
3525
+ <td class="chat-md-table-cell" [style.text-align]="alignment() ?? null">
3526
+ <chat-md-children [parent]="node()" />
3527
+ </td>
3528
+ }
3529
+ `,
3530
+ }]
3531
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
3080
3532
 
3081
- .chat-input__container {
3082
- width: 100%;
3083
- max-width: var(--tplane-chat-max-width);
3084
- margin: 0 auto;
3533
+ // libs/chat/src/lib/markdown/views/markdown-html.component.ts
3534
+ // SPDX-License-Identifier: MIT
3535
+ /**
3536
+ * Renders a `html-block` / `html-inline` markdown node as **escaped text** —
3537
+ * the raw HTML is shown literally (Angular interpolation auto-escapes it),
3538
+ * never injected as live markup. This preserves the pre-0.4 behavior where
3539
+ * raw HTML was plain text, and keeps the chat XSS-safe: model-emitted
3540
+ * `<script>`, `<iframe>`, etc. are displayed as text and never executed.
3541
+ */
3542
+ class MarkdownHtmlComponent {
3543
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
3544
+ raw = computed(() => this.node().raw, ...(ngDevMode ? [{ debugName: "raw" }] : []));
3545
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3546
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: MarkdownHtmlComponent, isStandalone: true, selector: "chat-md-html", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `{{ raw() }}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3085
3547
  }
3548
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, decorators: [{
3549
+ type: Component,
3550
+ args: [{
3551
+ selector: 'chat-md-html',
3552
+ standalone: true,
3553
+ changeDetection: ChangeDetectionStrategy.OnPush,
3554
+ template: `{{ raw() }}`,
3555
+ }]
3556
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
3086
3557
 
3087
- .chat-input__pill {
3088
- display: flex;
3089
- align-items: center;
3090
- gap: 8px;
3091
- background: var(--tplane-chat-surface);
3092
- border: 1px solid var(--tplane-chat-separator);
3093
- border-radius: 9999px;
3094
- padding: 8px 8px 8px 16px;
3095
- min-height: 56px;
3096
- box-sizing: border-box;
3097
- }
3098
-
3099
- .chat-input__textarea {
3100
- flex: 1 1 auto;
3101
- border: 0;
3102
- outline: none;
3103
- resize: none;
3104
- background: transparent;
3105
- color: var(--tplane-chat-text);
3106
- font: inherit;
3107
- font-size: 1rem;
3108
- line-height: 1.5;
3109
- padding: 0;
3110
- field-sizing: content;
3111
- overflow-y: auto;
3112
- }
3113
- .chat-input__textarea::placeholder { color: var(--tplane-chat-text-muted); }
3114
- .chat-input__textarea::-webkit-scrollbar { width: 4px; }
3115
- .chat-input__textarea::-webkit-scrollbar-thumb { background: var(--tplane-chat-separator); border-radius: 4px; }
3116
-
3117
- .chat-input__controls {
3118
- display: flex;
3119
- align-items: center;
3120
- gap: 4px;
3121
- flex: none;
3122
- }
3123
-
3124
- .chat-input__send,
3125
- .chat-input__send--stop {
3126
- width: 36px;
3127
- height: 36px;
3128
- border-radius: 50%;
3129
- border: 0;
3130
- display: flex;
3131
- align-items: center;
3132
- justify-content: center;
3133
- cursor: pointer;
3134
- transition: opacity 150ms ease, transform 150ms ease, background 150ms ease;
3135
- padding: 0;
3136
- }
3137
- .chat-input__send {
3138
- background: var(--tplane-chat-text);
3139
- color: var(--tplane-chat-bg);
3140
- }
3141
- .chat-input__send:disabled {
3142
- opacity: 0.35;
3143
- cursor: not-allowed;
3144
- background: var(--tplane-chat-text-muted);
3145
- }
3146
- .chat-input__send:not(:disabled):hover { transform: scale(1.05); }
3147
- .chat-input__send svg { width: 16px; height: 16px; }
3148
-
3149
- .chat-input__send--stop {
3150
- background: var(--tplane-chat-text-muted);
3151
- color: var(--tplane-chat-bg);
3152
- }
3153
- .chat-input__send--stop:hover { transform: scale(1.05); }
3154
- .chat-input__send--stop svg { width: 14px; height: 14px; }
3155
- `;
3558
+ // libs/chat/src/lib/markdown/cacheplane-markdown-views.ts
3559
+ // SPDX-License-Identifier: MIT
3560
+ /**
3561
+ * Default view registry consumed by <chat-streaming-md>. Maps every
3562
+ * MarkdownNode.type emitted by @cacheplane/partial-markdown@0.2 to its
3563
+ * corresponding Angular component.
3564
+ *
3565
+ * Override per-node-type via `withViews(cacheplaneMarkdownViews, { … })`.
3566
+ */
3567
+ const cacheplaneMarkdownViews = views({
3568
+ 'document': MarkdownDocumentComponent,
3569
+ 'paragraph': MarkdownParagraphComponent,
3570
+ 'heading': MarkdownHeadingComponent,
3571
+ 'blockquote': MarkdownBlockquoteComponent,
3572
+ 'list': MarkdownListComponent,
3573
+ 'list-item': MarkdownListItemComponent,
3574
+ 'code-block': MarkdownCodeBlockComponent,
3575
+ 'thematic-break': MarkdownThematicBreakComponent,
3576
+ 'text': MarkdownTextComponent,
3577
+ 'emphasis': MarkdownEmphasisComponent,
3578
+ 'strong': MarkdownStrongComponent,
3579
+ 'strikethrough': MarkdownStrikethroughComponent,
3580
+ 'inline-code': MarkdownInlineCodeComponent,
3581
+ 'math-inline': MarkdownMathComponent,
3582
+ 'math-display': MarkdownMathComponent,
3583
+ 'link': MarkdownLinkComponent,
3584
+ 'autolink': MarkdownAutolinkComponent,
3585
+ 'image': MarkdownImageComponent,
3586
+ 'soft-break': MarkdownSoftBreakComponent,
3587
+ 'hard-break': MarkdownHardBreakComponent,
3588
+ 'citation-reference': MarkdownCitationReferenceComponent,
3589
+ 'table': MarkdownTableComponent,
3590
+ 'table-row': MarkdownTableRowComponent,
3591
+ 'table-cell': MarkdownTableCellComponent,
3592
+ // Raw HTML (added by partial-markdown 0.4.x) renders as escaped literal text
3593
+ // — XSS-safe, and matches the pre-0.4 behavior where HTML was plain text.
3594
+ 'html-block': MarkdownHtmlComponent,
3595
+ 'html-inline': MarkdownHtmlComponent,
3596
+ });
3156
3597
 
3157
- // libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
3598
+ // libs/chat/src/lib/streaming/streaming-markdown.component.ts
3158
3599
  // SPDX-License-Identifier: MIT
3600
+ // How long streaming must be false AND content stable before we finalize the
3601
+ // parser. finish() is only needed to mark final node status (not used visually)
3602
+ // and to revert a genuinely-truncated trailing construct to CommonMark — the
3603
+ // live `parser.root` projection already renders everything during streaming, so
3604
+ // this delay has NO visual cost. It must comfortably exceed real inter-chunk
3605
+ // gaps (e.g. the pause between a table's header row and its delimiter row) and
3606
+ // any `streaming` flag flap, so finalize never fires mid-stream and reverts an
3607
+ // in-progress table to raw "| a | b |" text.
3608
+ const FINALIZE_DEBOUNCE_MS = 600;
3159
3609
  /**
3160
- * Submits a trimmed message to the agent.
3161
- * Returns the trimmed string on success, or `null` if the input was empty.
3610
+ * Renders streaming markdown by walking a @cacheplane/partial-markdown AST
3611
+ * through @threadplane/render's view registry.
3612
+ *
3613
+ * Reactivity model: the live `parser.root` keeps a stable JS reference
3614
+ * across pushes (partial-markdown's identity guarantee). To make Angular
3615
+ * signals propagate downstream when the underlying tree changes, we surface
3616
+ * a materialized snapshot via `materialize()`. The snapshot shares
3617
+ * structurally — unchanged subtrees keep the SAME reference, and any
3618
+ * descendant change yields a NEW root reference. This lets Angular's
3619
+ * `Object.is` equality check both detect changes (root reference differs)
3620
+ * and short-circuit unchanged subtrees (per-node references stable).
3621
+ *
3622
+ * Override per-node-type renderers via the `[viewRegistry]` input or by
3623
+ * supplying a different `MARKDOWN_VIEW_REGISTRY` provider in the injector
3624
+ * tree.
3162
3625
  */
3163
- function submitMessage(agent, text) {
3164
- const trimmed = text.trim();
3165
- if (!trimmed)
3166
- return null;
3167
- void agent.submit({ message: trimmed });
3168
- return trimmed;
3169
- }
3170
- class ChatInputComponent {
3171
- agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
3172
- submitOnEnter = input(true, ...(ngDevMode ? [{ debugName: "submitOnEnter" }] : []));
3173
- placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
3174
- /** When true (default), shows a stop button while the agent is streaming. */
3175
- showStopButton = input(true, ...(ngDevMode ? [{ debugName: "showStopButton" }] : []));
3176
- submitted = output();
3177
- stopped = output();
3178
- messageText = signal('', ...(ngDevMode ? [{ debugName: "messageText" }] : []));
3179
- isLoading = computed(() => this.agent().isLoading(), ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3180
- /** True while an IME composition (CJK input, accent, autocorrect) is active. */
3181
- composing = signal(false, ...(ngDevMode ? [{ debugName: "composing" }] : []));
3182
- focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : []));
3183
- /** Submit is allowed only when not loading and there's non-whitespace text. */
3184
- canSubmit = computed(() => {
3185
- if (this.isLoading())
3186
- return false;
3187
- return this.messageText().trim().length > 0;
3188
- }, ...(ngDevMode ? [{ debugName: "canSubmit" }] : []));
3189
- /** The stop button only appears when the consumer opted in AND we're loading. */
3190
- canStop = computed(() => this.showStopButton(), ...(ngDevMode ? [{ debugName: "canStop" }] : []));
3191
- textareaEl = viewChild('textareaEl', ...(ngDevMode ? [{ debugName: "textareaEl" }] : []));
3192
- /**
3193
- * Auto-resize the textarea to fit its content as the user types or pastes
3194
- * multi-line text. Caps at min(40vh, 320px); beyond that the textarea
3195
- * scrolls. Without this, multi-line input is hidden behind the rows="1"
3196
- * fixed height (caught by live browser smoke).
3197
- */
3626
+ class ChatStreamingMdComponent {
3627
+ content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
3628
+ streaming = input(false, ...(ngDevMode ? [{ debugName: "streaming" }] : []));
3629
+ viewRegistry = input(undefined, ...(ngDevMode ? [{ debugName: "viewRegistry" }] : []));
3630
+ resolvedRegistry = computed(() => this.viewRegistry() ?? cacheplaneMarkdownViews, ...(ngDevMode ? [{ debugName: "resolvedRegistry" }] : []));
3631
+ resolver = inject(CitationsResolverService, { optional: true });
3198
3632
  constructor() {
3199
3633
  effect(() => {
3200
- const text = this.messageText();
3201
- const el = this.textareaEl()?.nativeElement;
3202
- if (!el)
3634
+ const r = this.root();
3635
+ if (this.resolver && r) {
3636
+ this.resolver.markdownDefs.set(r.citations ?? new Map());
3637
+ }
3638
+ });
3639
+ // Debounced finalization. `finish()` is terminal and DESTRUCTIVE: it reverts
3640
+ // an incomplete trailing construct to its CommonMark fallback — e.g. a table
3641
+ // header before its delimiter row becomes raw "| a | b |" paragraph text. We
3642
+ // must therefore never finalize while tokens are still arriving. The
3643
+ // `streaming` input is not a reliable "still arriving" signal — it can flap
3644
+ // false mid-stream, and at cold start it can read false for an entire live
3645
+ // stream — so we finalize only once streaming is false AND no new content
3646
+ // has arrived for a short, imperceptible window. Any new content or a
3647
+ // streaming=true flap re-arms the timer, so finalize fires exactly once,
3648
+ // after the stream truly stops. Until then the live `parser.root` projection
3649
+ // (0.5.x) renders the in-progress content, including streaming tables.
3650
+ let timer = null;
3651
+ effect((onCleanup) => {
3652
+ const isStreaming = this.streaming();
3653
+ this.content(); // re-arm whenever new content arrives
3654
+ if (timer) {
3655
+ clearTimeout(timer);
3656
+ timer = null;
3657
+ }
3658
+ if (isStreaming || this.finished)
3203
3659
  return;
3204
- // Cap: min(40vh, 320px). Recomputed on each input so viewport resizes
3205
- // between keystrokes are picked up without a dedicated resize listener.
3206
- const viewportH = typeof window === 'undefined' ? 600 : window.innerHeight;
3207
- const cap = Math.min(viewportH * 0.4, 320);
3208
- el.style.height = 'auto';
3209
- const next = Math.min(el.scrollHeight, cap);
3210
- el.style.height = `${next}px`;
3211
- el.style.overflowY = el.scrollHeight > cap ? 'auto' : 'hidden';
3212
- void text;
3660
+ timer = setTimeout(() => {
3661
+ timer = null;
3662
+ if (this.streaming() || this.finished)
3663
+ return;
3664
+ if (!this.prior.endsWith('\n'))
3665
+ this.parser.push('\n');
3666
+ this.parser.finish();
3667
+ this.finished = true;
3668
+ this.finalizeTick.update((v) => v + 1);
3669
+ }, FINALIZE_DEBOUNCE_MS);
3670
+ onCleanup(() => {
3671
+ if (timer) {
3672
+ clearTimeout(timer);
3673
+ timer = null;
3674
+ }
3675
+ });
3213
3676
  });
3214
3677
  }
3215
- focusTextarea() {
3216
- this.textareaEl()?.nativeElement.focus();
3217
- }
3218
- onSubmit() {
3219
- const submitted = submitMessage(this.agent(), this.messageText());
3220
- if (submitted !== null) {
3221
- this.submitted.emit(submitted);
3222
- this.messageText.set('');
3223
- const el = this.textareaEl()?.nativeElement;
3224
- if (el)
3225
- el.value = '';
3226
- requestAnimationFrame(() => this.textareaEl()?.nativeElement.focus());
3227
- }
3228
- }
3229
- /** Sync the textarea's value into the signal on user input. A direct
3230
- * [value]/(input) pair is used instead of ngModel: NgModel does not
3231
- * reliably write a programmatic clear back to the view under zoneless
3232
- * + OnPush, leaving sent text visible in the composer (audit F1). */
3233
- onInput(event) {
3234
- this.messageText.set(event.target.value);
3235
- }
3236
- /** Abort the current streaming response (if the adapter supports it). */
3237
- onStop() {
3238
- const a = this.agent();
3239
- if (typeof a.stop === 'function') {
3240
- void a.stop();
3241
- }
3242
- this.stopped.emit();
3243
- }
3244
- onKeydown(event) {
3245
- if (!this.submitOnEnter() || event.shiftKey)
3246
- return;
3247
- // Don't submit while an IME composition is in progress (CJK input,
3248
- // dead-key accents, autocorrect popups). The composition's terminating
3249
- // Enter must reach the textarea so the candidate is committed; submitting
3250
- // here would discard the user's in-progress character.
3251
- if (this.composing() || event.isComposing || event.keyCode === 229)
3252
- return;
3253
- event.preventDefault();
3254
- this.onSubmit();
3678
+ // Parser instance is rebuilt only when content diverges from the prior
3679
+ // prefix (rare). For the common streaming case where content extends the
3680
+ // prior content, we push the delta and reuse the existing parser tree.
3681
+ parser = createPartialMarkdownParser();
3682
+ prior = '';
3683
+ finished = false;
3684
+ // Bumped by the debounced finalizer so the `root` computed re-materializes
3685
+ // the now-finished parser tree.
3686
+ finalizeTick = signal(0, ...(ngDevMode ? [{ debugName: "finalizeTick" }] : []));
3687
+ root = computed(() => {
3688
+ const c = this.content();
3689
+ this.finalizeTick(); // re-materialize after a debounced finalize
3690
+ if (c !== this.prior) {
3691
+ // Re-parse from scratch when the content diverged from the prior prefix,
3692
+ // OR when the parser was already finalized finish() is terminal, so
3693
+ // pushing further deltas into a finished parser corrupts its state. A
3694
+ // transient `streaming=false` mid-stream that finalized early thus
3695
+ // recovers here: new content rebuilds an open, projecting parser.
3696
+ if (c.startsWith(this.prior) && !this.finished) {
3697
+ this.parser.push(c.slice(this.prior.length));
3698
+ }
3699
+ else {
3700
+ this.parser = createPartialMarkdownParser();
3701
+ this.finished = false;
3702
+ if (c.length > 0)
3703
+ this.parser.push(c);
3704
+ }
3705
+ this.prior = c;
3706
+ }
3707
+ // Materialize for Angular reactivity: produces a NEW root reference when
3708
+ // any descendant subtree changed; same reference when nothing changed
3709
+ // (structural sharing). This is what makes signal-based CD propagate
3710
+ // downstream changes despite the parser preserving identity.
3711
+ return materialize(this.parser.root);
3712
+ }, ...(ngDevMode ? [{ debugName: "root" }] : []));
3713
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3714
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatStreamingMdComponent, isStandalone: true, selector: "chat-streaming-md", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, viewRegistry: { classPropertyName: "viewRegistry", publicName: "viewRegistry", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
3715
+ {
3716
+ provide: MARKDOWN_VIEW_REGISTRY,
3717
+ useFactory: (host) => host.resolvedRegistry(),
3718
+ deps: [ChatStreamingMdComponent],
3719
+ },
3720
+ ], ngImport: i0, template: `
3721
+ @if (root(); as r) {
3722
+ <chat-md-children [parent]="r" />
3255
3723
  }
3256
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3257
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatInputComponent, isStandalone: true, selector: "chat-input", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, submitOnEnter: { classPropertyName: "submitOnEnter", publicName: "submitOnEnter", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showStopButton: { classPropertyName: "showStopButton", publicName: "showStopButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", stopped: "stopped" }, viewQueries: [{ propertyName: "textareaEl", first: true, predicate: ["textareaEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
3258
- <div class="chat-input__container">
3259
- <ng-content select="[chatInputBanner]" />
3260
- <ng-content select="[chatInputAttachments]" />
3261
- <div class="chat-input__pill">
3262
- <ng-content select="[chatInputLeading]" />
3263
- <textarea
3264
- #textareaEl
3265
- class="chat-input__textarea"
3266
- [value]="messageText()"
3267
- (input)="onInput($event)"
3268
- [placeholder]="placeholder()"
3269
- (keydown.enter)="onKeydown($any($event))"
3270
- (compositionstart)="composing.set(true)"
3271
- (compositionend)="composing.set(false)"
3272
- (focus)="focused.set(true)"
3273
- (blur)="focused.set(false)"
3274
- name="messageText"
3275
- rows="1"
3276
- aria-label="Type a message"
3277
- ></textarea>
3278
- <div class="chat-input__controls">
3279
- <ng-content select="[chatInputModelSelect]" />
3280
- <ng-content select="[chatInputTrailing]" />
3281
- @if (isLoading() && canStop()) {
3282
- <button
3283
- type="button"
3284
- class="chat-input__send chat-input__send--stop"
3285
- (click)="onStop()"
3286
- aria-label="Stop generating"
3287
- title="Stop generating"
3288
- >
3289
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
3290
- <rect x="6" y="6" width="12" height="12" rx="2"/>
3291
- </svg>
3292
- </button>
3293
- } @else {
3294
- <button
3295
- type="button"
3296
- class="chat-input__send"
3297
- [disabled]="!canSubmit()"
3298
- (click)="onSubmit()"
3299
- aria-label="Send message"
3300
- >
3301
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3302
- <line x1="12" y1="19" x2="12" y2="5"/>
3303
- <polyline points="5 12 12 5 19 12"/>
3304
- </svg>
3305
- </button>
3306
- }
3307
- </div>
3308
- </div>
3309
- <ng-content select="[chatInputFooter]" />
3310
- </div>
3311
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;width:100%;padding:0 var(--tplane-chat-edge-pad);box-sizing:border-box}.chat-input__container{width:100%;max-width:var(--tplane-chat-max-width);margin:0 auto}.chat-input__pill{display:flex;align-items:center;gap:8px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:9999px;padding:8px 8px 8px 16px;min-height:56px;box-sizing:border-box}.chat-input__textarea{flex:1 1 auto;border:0;outline:none;resize:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem;line-height:1.5;padding:0;field-sizing:content;overflow-y:auto}.chat-input__textarea::placeholder{color:var(--tplane-chat-text-muted)}.chat-input__textarea::-webkit-scrollbar{width:4px}.chat-input__textarea::-webkit-scrollbar-thumb{background:var(--tplane-chat-separator);border-radius:4px}.chat-input__controls{display:flex;align-items:center;gap:4px;flex:none}.chat-input__send,.chat-input__send--stop{width:36px;height:36px;border-radius:50%;border:0;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:opacity .15s ease,transform .15s ease,background .15s ease;padding:0}.chat-input__send{background:var(--tplane-chat-text);color:var(--tplane-chat-bg)}.chat-input__send:disabled{opacity:.35;cursor:not-allowed;background:var(--tplane-chat-text-muted)}.chat-input__send:not(:disabled):hover{transform:scale(1.05)}.chat-input__send svg{width:16px;height:16px}.chat-input__send--stop{background:var(--tplane-chat-text-muted);color:var(--tplane-chat-bg)}.chat-input__send--stop:hover{transform:scale(1.05)}.chat-input__send--stop svg{width:14px;height:14px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3724
+ `, isInline: true, styles: ["chat-streaming-md{display:block;color:var(--tplane-chat-text);line-height:var(--tplane-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--tplane-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--tplane-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--tplane-chat-text-muted)}chat-streaming-md mark{background:var(--tplane-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--tplane-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--tplane-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--tplane-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:12px 14px;border-radius:var(--tplane-chat-radius-card);overflow-x:auto;font-family:var(--tplane-chat-font-mono);font-size:var(--tplane-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--tplane-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--tplane-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--tplane-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--tplane-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--tplane-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--tplane-chat-surface-alt);border:1px dashed var(--tplane-chat-separator);border-radius:6px;font-size:.9em;color:var(--tplane-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--tplane-chat-font-mono, ui-monospace, monospace)}\n"], dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3312
3725
  }
3313
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatInputComponent, decorators: [{
3726
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, decorators: [{
3314
3727
  type: Component,
3315
- args: [{ selector: 'chat-input', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: `
3316
- <div class="chat-input__container">
3317
- <ng-content select="[chatInputBanner]" />
3318
- <ng-content select="[chatInputAttachments]" />
3319
- <div class="chat-input__pill">
3320
- <ng-content select="[chatInputLeading]" />
3321
- <textarea
3322
- #textareaEl
3323
- class="chat-input__textarea"
3324
- [value]="messageText()"
3325
- (input)="onInput($event)"
3326
- [placeholder]="placeholder()"
3327
- (keydown.enter)="onKeydown($any($event))"
3328
- (compositionstart)="composing.set(true)"
3329
- (compositionend)="composing.set(false)"
3330
- (focus)="focused.set(true)"
3331
- (blur)="focused.set(false)"
3332
- name="messageText"
3333
- rows="1"
3334
- aria-label="Type a message"
3335
- ></textarea>
3336
- <div class="chat-input__controls">
3337
- <ng-content select="[chatInputModelSelect]" />
3338
- <ng-content select="[chatInputTrailing]" />
3339
- @if (isLoading() && canStop()) {
3340
- <button
3341
- type="button"
3342
- class="chat-input__send chat-input__send--stop"
3343
- (click)="onStop()"
3344
- aria-label="Stop generating"
3345
- title="Stop generating"
3346
- >
3347
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
3348
- <rect x="6" y="6" width="12" height="12" rx="2"/>
3349
- </svg>
3350
- </button>
3351
- } @else {
3352
- <button
3353
- type="button"
3354
- class="chat-input__send"
3355
- [disabled]="!canSubmit()"
3356
- (click)="onSubmit()"
3357
- aria-label="Send message"
3358
- >
3359
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3360
- <line x1="12" y1="19" x2="12" y2="5"/>
3361
- <polyline points="5 12 12 5 19 12"/>
3362
- </svg>
3363
- </button>
3364
- }
3365
- </div>
3366
- </div>
3367
- <ng-content select="[chatInputFooter]" />
3368
- </div>
3369
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;width:100%;padding:0 var(--tplane-chat-edge-pad);box-sizing:border-box}.chat-input__container{width:100%;max-width:var(--tplane-chat-max-width);margin:0 auto}.chat-input__pill{display:flex;align-items:center;gap:8px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:9999px;padding:8px 8px 8px 16px;min-height:56px;box-sizing:border-box}.chat-input__textarea{flex:1 1 auto;border:0;outline:none;resize:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem;line-height:1.5;padding:0;field-sizing:content;overflow-y:auto}.chat-input__textarea::placeholder{color:var(--tplane-chat-text-muted)}.chat-input__textarea::-webkit-scrollbar{width:4px}.chat-input__textarea::-webkit-scrollbar-thumb{background:var(--tplane-chat-separator);border-radius:4px}.chat-input__controls{display:flex;align-items:center;gap:4px;flex:none}.chat-input__send,.chat-input__send--stop{width:36px;height:36px;border-radius:50%;border:0;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:opacity .15s ease,transform .15s ease,background .15s ease;padding:0}.chat-input__send{background:var(--tplane-chat-text);color:var(--tplane-chat-bg)}.chat-input__send:disabled{opacity:.35;cursor:not-allowed;background:var(--tplane-chat-text-muted)}.chat-input__send:not(:disabled):hover{transform:scale(1.05)}.chat-input__send svg{width:16px;height:16px}.chat-input__send--stop{background:var(--tplane-chat-text-muted);color:var(--tplane-chat-bg)}.chat-input__send--stop:hover{transform:scale(1.05)}.chat-input__send--stop svg{width:14px;height:14px}\n"] }]
3370
- }], ctorParameters: () => [], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }], submitOnEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnEnter", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showStopButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showStopButton", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], stopped: [{ type: i0.Output, args: ["stopped"] }], textareaEl: [{ type: i0.ViewChild, args: ['textareaEl', { isSignal: true }] }] } });
3728
+ args: [{ selector: 'chat-streaming-md', standalone: true, imports: [MarkdownChildrenComponent], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
3729
+ @if (root(); as r) {
3730
+ <chat-md-children [parent]="r" />
3731
+ }
3732
+ `, providers: [
3733
+ {
3734
+ provide: MARKDOWN_VIEW_REGISTRY,
3735
+ useFactory: (host) => host.resolvedRegistry(),
3736
+ deps: [ChatStreamingMdComponent],
3737
+ },
3738
+ ], styles: ["chat-streaming-md{display:block;color:var(--tplane-chat-text);line-height:var(--tplane-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--tplane-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--tplane-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--tplane-chat-text-muted)}chat-streaming-md mark{background:var(--tplane-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--tplane-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--tplane-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--tplane-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--tplane-chat-surface-alt);color:var(--tplane-chat-text);padding:12px 14px;border-radius:var(--tplane-chat-radius-card);overflow-x:auto;font-family:var(--tplane-chat-font-mono);font-size:var(--tplane-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--tplane-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--tplane-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--tplane-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--tplane-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--tplane-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--tplane-chat-surface-alt);border:1px dashed var(--tplane-chat-separator);border-radius:6px;font-size:.9em;color:var(--tplane-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--tplane-chat-font-mono, ui-monospace, monospace)}\n"] }]
3739
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], viewRegistry: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewRegistry", required: false }] }] } });
3371
3740
 
3372
3741
  // SPDX-License-Identifier: MIT
3373
- const CHAT_TYPING_INDICATOR_STYLES = `
3374
- /* Sit in the same centered column as chat-message-list so the dots
3375
- don't flash at the scroll container's left edge before the assistant
3376
- message renders. */
3377
- :host {
3378
- display: block;
3379
- padding: 0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);
3380
- max-width: var(--tplane-chat-max-width);
3381
- margin: 0 auto;
3382
- width: 100%;
3383
- box-sizing: border-box;
3384
- }
3385
- .chat-typing__dots { display: inline-flex; gap: 4px; align-items: center; }
3386
- .chat-typing__dot {
3387
- width: 6px;
3388
- height: 6px;
3389
- border-radius: 50%;
3390
- background: var(--tplane-chat-text-muted);
3391
- animation: tplane-chat-typing-dot 1.4s ease-in-out infinite both;
3392
- }
3393
- .chat-typing__dot:nth-child(2) { animation-delay: 0.2s; }
3394
- .chat-typing__dot:nth-child(3) { animation-delay: 0.4s; }
3395
- `;
3742
+ /**
3743
+ * Render a millisecond duration as a human-readable label suitable for
3744
+ * the chat-reasoning "Thought for Ns" pill.
3745
+ *
3746
+ * - <1 s → "<1s"
3747
+ * - 1–59 s → "Ns" (e.g. "4s")
3748
+ * - ≥60 s → "Nm Ms" (e.g. "1m 12s", "60m 0s")
3749
+ *
3750
+ * Negative or non-finite inputs collapse to "<1s" so a corrupted timing
3751
+ * map never produces noisy output.
3752
+ */
3753
+ function formatDuration(ms) {
3754
+ if (!Number.isFinite(ms) || ms < 1000)
3755
+ return '<1s';
3756
+ const totalSeconds = Math.floor(ms / 1000);
3757
+ if (totalSeconds < 60)
3758
+ return `${totalSeconds}s`;
3759
+ const minutes = Math.floor(totalSeconds / 60);
3760
+ const seconds = totalSeconds - minutes * 60;
3761
+ return `${minutes}m ${seconds}s`;
3762
+ }
3396
3763
 
3397
- // libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts
3764
+ // libs/chat/src/lib/primitives/chat-reasoning/chat-reasoning.component.ts
3398
3765
  // SPDX-License-Identifier: MIT
3399
3766
  /**
3400
- * Whether the agent should show a "typing" indicator — it is loading and has
3401
- * not yet started streaming the assistant's reply.
3767
+ * Renders an assistant's reasoning content as a compact pill that
3768
+ * expands to reveal the underlying text. Three visual states:
3402
3769
  *
3403
- * @param agent The agent to inspect.
3404
- * @returns `true` while the agent is awaiting a response but no assistant text
3405
- * has streamed yet; `false` once tokens arrive or the agent is idle.
3406
- * @example
3407
- * ```ts
3408
- * \@if (isTyping(agent)) { <chat-typing-indicator [agent]="agent" /> }
3409
- * ```
3770
+ * - Streaming: pill shows "Thinking…" with a pulsing dot; auto-expanded
3771
+ * so the user sees reasoning stream in real time.
3772
+ * - Idle, with durationMs known: pill shows "Thought for {duration}";
3773
+ * collapsed by default, expand on click.
3774
+ * - Idle, no duration: pill shows "Show reasoning"; collapsed by default.
3775
+ *
3776
+ * The body re-uses chat-streaming-md so reasoning content gets the same
3777
+ * markdown rendering pipeline as the visible response (lists, code,
3778
+ * step labels often appear in reasoning output).
3779
+ *
3780
+ * Internal state: a tristate "expanded" — null means follow auto state-
3781
+ * driven logic (force-expand on isStreaming, otherwise honor
3782
+ * defaultExpanded), boolean is a manual user choice that wins for the
3783
+ * lifetime of the instance.
3410
3784
  */
3411
- function isTyping(agent) {
3412
- if (!agent.isLoading())
3413
- return false;
3414
- const msgs = agent.messages();
3415
- if (msgs.length === 0)
3416
- return true;
3417
- const last = msgs[msgs.length - 1];
3418
- if (last.role === 'user')
3419
- return true;
3420
- if (last.role === 'assistant') {
3421
- return typeof last.content === 'string'
3422
- ? !last.content
3423
- : last.content.length === 0;
3785
+ class ChatReasoningComponent {
3786
+ content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
3787
+ isStreaming = input(false, ...(ngDevMode ? [{ debugName: "isStreaming" }] : []));
3788
+ durationMs = input(undefined, ...(ngDevMode ? [{ debugName: "durationMs" }] : []));
3789
+ label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
3790
+ defaultExpanded = input(false, ...(ngDevMode ? [{ debugName: "defaultExpanded" }] : []));
3791
+ hasContent = computed(() => (this.content() ?? '').length > 0, ...(ngDevMode ? [{ debugName: "hasContent" }] : []));
3792
+ /** null = follow auto logic (streaming expanded, else defaultExpanded). */
3793
+ _expandedOverride = signal(null, ...(ngDevMode ? [{ debugName: "_expandedOverride" }] : []));
3794
+ expanded = computed(() => {
3795
+ const override = this._expandedOverride();
3796
+ if (override !== null)
3797
+ return override;
3798
+ if (this.isStreaming())
3799
+ return true;
3800
+ return this.defaultExpanded();
3801
+ }, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
3802
+ expandedStr = computed(() => String(this.expanded()), ...(ngDevMode ? [{ debugName: "expandedStr" }] : []));
3803
+ resolvedLabel = computed(() => {
3804
+ const explicit = this.label();
3805
+ if (explicit)
3806
+ return explicit;
3807
+ if (this.isStreaming())
3808
+ return 'Thinking…';
3809
+ const ms = this.durationMs();
3810
+ if (typeof ms === 'number')
3811
+ return `Thought for ${formatDuration(ms)}`;
3812
+ return 'Show reasoning';
3813
+ }, ...(ngDevMode ? [{ debugName: "resolvedLabel" }] : []));
3814
+ constructor() {
3815
+ // Reset the manual override when streaming re-engages from idle (e.g.
3816
+ // follow-up turn that re-uses this instance) so the auto force-expand
3817
+ // logic takes over again. Spec §3.3 bullet 3.
3818
+ let prevStreaming = false;
3819
+ effect(() => {
3820
+ const streaming = this.isStreaming();
3821
+ if (!prevStreaming && streaming) {
3822
+ this._expandedOverride.set(null);
3823
+ }
3824
+ prevStreaming = streaming;
3825
+ });
3424
3826
  }
3425
- return false;
3426
- }
3427
- class ChatTypingIndicatorComponent {
3428
- agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
3429
- visible = computed(() => isTyping(this.agent()), ...(ngDevMode ? [{ debugName: "visible" }] : []));
3430
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatTypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3431
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatTypingIndicatorComponent, isStandalone: true, selector: "chat-typing-indicator", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3432
- @if (visible()) {
3433
- <div class="chat-typing__dots" role="status" aria-label="Assistant is typing">
3434
- <span class="chat-typing__dot"></span>
3435
- <span class="chat-typing__dot"></span>
3436
- <span class="chat-typing__dot"></span>
3827
+ toggle() {
3828
+ this._expandedOverride.set(!this.expanded());
3829
+ }
3830
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatReasoningComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3831
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatReasoningComponent, isStandalone: true, selector: "chat-reasoning", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, isStreaming: { classPropertyName: "isStreaming", publicName: "isStreaming", isSignal: true, isRequired: false, transformFunction: null }, durationMs: { classPropertyName: "durationMs", publicName: "durationMs", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, defaultExpanded: { classPropertyName: "defaultExpanded", publicName: "defaultExpanded", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-has-content": "hasContent()", "attr.data-expanded": "expandedStr()", "attr.data-streaming": "isStreaming()" } }, ngImport: i0, template: `
3832
+ <button
3833
+ type="button"
3834
+ class="chat-reasoning__header"
3835
+ [attr.aria-expanded]="expanded()"
3836
+ (click)="toggle()"
3837
+ >
3838
+ <svg class="chat-reasoning__chevron" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3839
+ <path d="M4 2l4 4-4 4"/>
3840
+ </svg>
3841
+ @if (isStreaming()) {
3842
+ <span class="chat-reasoning__pulse" aria-hidden="true"></span>
3843
+ }
3844
+ <span class="chat-reasoning__label">{{ resolvedLabel() }}</span>
3845
+ </button>
3846
+ @if (expanded()) {
3847
+ <div class="chat-reasoning__body">
3848
+ <chat-streaming-md [content]="content()" [streaming]="isStreaming()" />
3437
3849
  </div>
3438
3850
  }
3439
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;padding:0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);max-width:var(--tplane-chat-max-width);margin:0 auto;width:100%;box-sizing:border-box}.chat-typing__dots{display:inline-flex;gap:4px;align-items:center}.chat-typing__dot{width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:tplane-chat-typing-dot 1.4s ease-in-out infinite both}.chat-typing__dot:nth-child(2){animation-delay:.2s}.chat-typing__dot:nth-child(3){animation-delay:.4s}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3851
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;margin:0 0 .5rem}:host([data-has-content=\"false\"]){display:none}.chat-reasoning__header{display:inline-flex;align-items:center;gap:.5rem;padding:4px 10px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:9999px;color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-xs);font-family:inherit;cursor:pointer;line-height:1.2}.chat-reasoning__header:hover{color:var(--tplane-chat-text)}.chat-reasoning__chevron{width:10px;height:10px;transition:transform .12s ease}:host([data-expanded=\"true\"]) .chat-reasoning__chevron{transform:rotate(90deg)}.chat-reasoning__pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:chat-reasoning-pulse 1.2s ease-in-out infinite}@keyframes chat-reasoning-pulse{0%,to{opacity:.3}50%{opacity:1}}.chat-reasoning__body{margin-top:.5rem;padding-left:12px;border-left:2px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted)}.chat-reasoning__body chat-streaming-md{font-size:.95em}\n"], dependencies: [{ kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3440
3852
  }
3441
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatTypingIndicatorComponent, decorators: [{
3853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatReasoningComponent, decorators: [{
3442
3854
  type: Component,
3443
- args: [{ selector: 'chat-typing-indicator', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3444
- @if (visible()) {
3445
- <div class="chat-typing__dots" role="status" aria-label="Assistant is typing">
3446
- <span class="chat-typing__dot"></span>
3447
- <span class="chat-typing__dot"></span>
3448
- <span class="chat-typing__dot"></span>
3855
+ args: [{ selector: 'chat-reasoning', standalone: true, imports: [ChatStreamingMdComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
3856
+ '[attr.data-has-content]': 'hasContent()',
3857
+ '[attr.data-expanded]': 'expandedStr()',
3858
+ '[attr.data-streaming]': 'isStreaming()',
3859
+ }, template: `
3860
+ <button
3861
+ type="button"
3862
+ class="chat-reasoning__header"
3863
+ [attr.aria-expanded]="expanded()"
3864
+ (click)="toggle()"
3865
+ >
3866
+ <svg class="chat-reasoning__chevron" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3867
+ <path d="M4 2l4 4-4 4"/>
3868
+ </svg>
3869
+ @if (isStreaming()) {
3870
+ <span class="chat-reasoning__pulse" aria-hidden="true"></span>
3871
+ }
3872
+ <span class="chat-reasoning__label">{{ resolvedLabel() }}</span>
3873
+ </button>
3874
+ @if (expanded()) {
3875
+ <div class="chat-reasoning__body">
3876
+ <chat-streaming-md [content]="content()" [streaming]="isStreaming()" />
3449
3877
  </div>
3450
3878
  }
3451
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;padding:0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);max-width:var(--tplane-chat-max-width);margin:0 auto;width:100%;box-sizing:border-box}.chat-typing__dots{display:inline-flex;gap:4px;align-items:center}.chat-typing__dot{width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:tplane-chat-typing-dot 1.4s ease-in-out infinite both}.chat-typing__dot:nth-child(2){animation-delay:.2s}.chat-typing__dot:nth-child(3){animation-delay:.4s}\n"] }]
3452
- }], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }] } });
3879
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;margin:0 0 .5rem}:host([data-has-content=\"false\"]){display:none}.chat-reasoning__header{display:inline-flex;align-items:center;gap:.5rem;padding:4px 10px;background:var(--tplane-chat-surface-alt);border:1px solid var(--tplane-chat-separator);border-radius:9999px;color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-xs);font-family:inherit;cursor:pointer;line-height:1.2}.chat-reasoning__header:hover{color:var(--tplane-chat-text)}.chat-reasoning__chevron{width:10px;height:10px;transition:transform .12s ease}:host([data-expanded=\"true\"]) .chat-reasoning__chevron{transform:rotate(90deg)}.chat-reasoning__pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:chat-reasoning-pulse 1.2s ease-in-out infinite}@keyframes chat-reasoning-pulse{0%,to{opacity:.3}50%{opacity:1}}.chat-reasoning__body{margin-top:.5rem;padding-left:12px;border-left:2px solid var(--tplane-chat-separator);color:var(--tplane-chat-text-muted)}.chat-reasoning__body chat-streaming-md{font-size:.95em}\n"] }]
3880
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], isStreaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStreaming", required: false }] }], durationMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "durationMs", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], defaultExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultExpanded", required: false }] }] } });
3453
3881
 
3454
- // libs/chat/src/lib/styles/chat-history-search-palette.styles.ts
3882
+ // libs/chat/src/lib/styles/chat-launcher-button.styles.ts
3455
3883
  // SPDX-License-Identifier: MIT
3456
- const CHAT_HISTORY_SEARCH_PALETTE_STYLES = `
3457
- :host { display: contents; }
3458
- .chat-history-search-palette__scrim {
3459
- position: fixed;
3460
- inset: 0;
3461
- background: rgba(0, 0, 0, 0.4);
3462
- z-index: var(--tplane-chat-z-modal-scrim, 1100);
3884
+ const CHAT_LAUNCHER_BUTTON_STYLES = `
3885
+ :host { display: inline-block; }
3886
+ .chat-launcher-button {
3887
+ width: 56px;
3888
+ height: 56px;
3889
+ border-radius: var(--tplane-chat-radius-launcher);
3890
+ background: var(--tplane-chat-primary);
3891
+ color: var(--tplane-chat-on-primary);
3463
3892
  border: 0;
3464
- padding: 0;
3465
3893
  cursor: pointer;
3466
- }
3467
- .chat-history-search-palette {
3468
- position: fixed;
3469
- top: 15vh;
3470
- left: 50%;
3471
- transform: translateX(-50%);
3472
- width: min(560px, 90vw);
3473
- max-height: 70vh;
3474
- background: var(--tplane-chat-bg);
3475
- border: 1px solid var(--tplane-chat-separator);
3476
- border-radius: 12px;
3477
- box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
3478
- z-index: var(--tplane-chat-z-modal, 1101);
3479
- display: flex;
3480
- flex-direction: column;
3481
- overflow: hidden;
3482
- }
3483
- .chat-history-search-palette__input-row {
3484
3894
  display: flex;
3485
3895
  align-items: center;
3486
- gap: 8px;
3487
- padding: 12px 16px;
3488
- border-bottom: 1px solid var(--tplane-chat-separator);
3489
- }
3490
- .chat-history-search-palette__icon {
3491
- width: 18px;
3492
- height: 18px;
3493
- color: var(--tplane-chat-text-muted);
3494
- flex-shrink: 0;
3896
+ justify-content: center;
3897
+ box-shadow: var(--tplane-chat-shadow-md);
3898
+ transition: transform 200ms ease;
3495
3899
  }
3496
- .chat-history-search-palette__input {
3497
- flex: 1 1 auto;
3498
- border: 0;
3499
- outline: none;
3900
+ .chat-launcher-button:hover { transform: scale(1.05); }
3901
+ .chat-launcher-button svg { width: 24px; height: 24px; }
3902
+ `;
3903
+
3904
+ // SPDX-License-Identifier: MIT
3905
+ class ChatLauncherButtonComponent {
3906
+ /** Fires when the inner <button> receives a click. Prefer this over
3907
+ * binding `(click)` on the host element — explicit output gives
3908
+ * consumers (and Playwright) an unambiguous click target that won't
3909
+ * be intercepted by sibling overlays in higher stacking contexts.
3910
+ * Native `(click)` on the host still works for back-compat: the
3911
+ * click event bubbles through unchanged. */
3912
+ clicked = output();
3913
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatLauncherButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3914
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.6", type: ChatLauncherButtonComponent, isStandalone: true, selector: "chat-launcher-button", outputs: { clicked: "clicked" }, ngImport: i0, template: `
3915
+ <button type="button" class="chat-launcher-button" aria-label="Open chat" (click)="clicked.emit()">
3916
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3917
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3918
+ </svg>
3919
+ </button>
3920
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline-block}.chat-launcher-button{width:56px;height:56px;border-radius:var(--tplane-chat-radius-launcher);background:var(--tplane-chat-primary);color:var(--tplane-chat-on-primary);border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--tplane-chat-shadow-md);transition:transform .2s ease}.chat-launcher-button:hover{transform:scale(1.05)}.chat-launcher-button svg{width:24px;height:24px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3921
+ }
3922
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatLauncherButtonComponent, decorators: [{
3923
+ type: Component,
3924
+ args: [{ selector: 'chat-launcher-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3925
+ <button type="button" class="chat-launcher-button" aria-label="Open chat" (click)="clicked.emit()">
3926
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3927
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3928
+ </svg>
3929
+ </button>
3930
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline-block}.chat-launcher-button{width:56px;height:56px;border-radius:var(--tplane-chat-radius-launcher);background:var(--tplane-chat-primary);color:var(--tplane-chat-on-primary);border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--tplane-chat-shadow-md);transition:transform .2s ease}.chat-launcher-button:hover{transform:scale(1.05)}.chat-launcher-button svg{width:24px;height:24px}\n"] }]
3931
+ }], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
3932
+
3933
+ // SPDX-License-Identifier: MIT
3934
+ const CHAT_SUGGESTIONS_STYLES = `
3935
+ :host { display: block; }
3936
+ .chat-suggestions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
3937
+ .chat-suggestion {
3938
+ padding: 6px 10px;
3939
+ font-size: var(--tplane-chat-font-size-xs);
3940
+ border-radius: var(--tplane-chat-radius-bubble);
3941
+ border: 1px solid var(--tplane-chat-muted);
3500
3942
  background: transparent;
3501
3943
  color: var(--tplane-chat-text);
3502
- font: inherit;
3503
- font-size: 1rem;
3504
- }
3505
- .chat-history-search-palette__input::placeholder {
3506
- color: var(--tplane-chat-text-muted);
3507
- }
3508
- .chat-history-search-palette__close {
3509
- background: transparent;
3510
- border: 0;
3511
- padding: 4px;
3512
- color: var(--tplane-chat-text-muted);
3513
- cursor: pointer;
3514
- border-radius: 4px;
3515
- }
3516
- .chat-history-search-palette__close:hover { color: var(--tplane-chat-text); }
3517
- .chat-history-search-palette__list {
3518
- flex: 1 1 auto;
3519
- overflow-y: auto;
3520
- padding: 4px;
3521
- margin: 0;
3522
- list-style: none;
3523
- }
3524
- .chat-history-search-palette__row {
3525
- display: flex;
3526
- flex-direction: column;
3527
- padding: 10px 12px;
3528
- border-radius: 8px;
3529
3944
  cursor: pointer;
3945
+ transition: transform 200ms ease;
3530
3946
  }
3531
- .chat-history-search-palette__row[aria-selected="true"] {
3532
- background: var(--tplane-chat-surface-alt);
3533
- }
3534
- .chat-history-search-palette__row-title {
3535
- color: var(--tplane-chat-text);
3536
- font-size: var(--tplane-chat-font-size);
3537
- }
3538
- .chat-history-search-palette__row-subtitle {
3539
- color: var(--tplane-chat-text-muted);
3540
- font-size: var(--tplane-chat-font-size-sm);
3541
- margin-top: 2px;
3542
- }
3543
- .chat-history-search-palette__empty,
3544
- .chat-history-search-palette__hint {
3545
- padding: 24px 16px;
3546
- color: var(--tplane-chat-text-muted);
3547
- text-align: center;
3548
- font-size: var(--tplane-chat-font-size-sm);
3549
- }
3550
- .chat-history-search-palette__skeleton {
3551
- padding: 8px 4px;
3552
- }
3553
- .chat-history-search-palette__skeleton-row {
3554
- height: 36px;
3555
- margin: 4px 0;
3556
- background: var(--tplane-chat-surface-alt);
3557
- border-radius: 8px;
3558
- animation: tplane-chat-pulse 1.4s ease-in-out infinite;
3559
- }
3947
+ .chat-suggestion:hover { transform: scale(1.03); }
3948
+ .chat-suggestion:disabled { cursor: wait; opacity: 0.6; }
3560
3949
  `;
3561
3950
 
3562
- // libs/chat/src/lib/primitives/chat-history-search-palette/chat-history-search-palette.component.ts
3563
3951
  // SPDX-License-Identifier: MIT
3564
- let paletteInstanceCounter = 0;
3565
- class ChatHistorySearchPaletteComponent {
3566
- open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
3567
- query = model('', ...(ngDevMode ? [{ debugName: "query" }] : []));
3568
- results = input([], ...(ngDevMode ? [{ debugName: "results" }] : []));
3569
- loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
3570
- placeholder = input('Search conversations', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
3571
- threadSelected = output();
3572
- closed = output();
3573
- activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : []));
3574
- listId = `chat-history-search-palette__results-${++paletteInstanceCounter}`;
3575
- inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
3952
+ class ChatSuggestionsComponent {
3953
+ suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
3954
+ selected = output();
3955
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3956
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatSuggestionsComponent, isStandalone: true, selector: "chat-suggestions", inputs: { suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: `
3957
+ <div class="chat-suggestions">
3958
+ @for (s of suggestions(); track s) {
3959
+ <button type="button" class="chat-suggestion" (click)="selected.emit(s)">{{ s }}</button>
3960
+ }
3961
+ </div>
3962
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}.chat-suggestion{padding:6px 10px;font-size:var(--tplane-chat-font-size-xs);border-radius:var(--tplane-chat-radius-bubble);border:1px solid var(--tplane-chat-muted);background:transparent;color:var(--tplane-chat-text);cursor:pointer;transition:transform .2s ease}.chat-suggestion:hover{transform:scale(1.03)}.chat-suggestion:disabled{cursor:wait;opacity:.6}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3963
+ }
3964
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSuggestionsComponent, decorators: [{
3965
+ type: Component,
3966
+ args: [{ selector: 'chat-suggestions', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3967
+ <div class="chat-suggestions">
3968
+ @for (s of suggestions(); track s) {
3969
+ <button type="button" class="chat-suggestion" (click)="selected.emit(s)">{{ s }}</button>
3970
+ }
3971
+ </div>
3972
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block}.chat-suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}.chat-suggestion{padding:6px 10px;font-size:var(--tplane-chat-font-size-xs);border-radius:var(--tplane-chat-radius-bubble);border:1px solid var(--tplane-chat-muted);background:transparent;color:var(--tplane-chat-text);cursor:pointer;transition:transform .2s ease}.chat-suggestion:hover{transform:scale(1.03)}.chat-suggestion:disabled{cursor:wait;opacity:.6}\n"] }]
3973
+ }], propDecorators: { suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
3974
+
3975
+ // libs/chat/src/lib/styles/chat-input.styles.ts
3976
+ // SPDX-License-Identifier: MIT
3977
+ const CHAT_INPUT_STYLES = `
3978
+ :host {
3979
+ display: block;
3980
+ width: 100%;
3981
+ padding: 0 var(--tplane-chat-edge-pad);
3982
+ box-sizing: border-box;
3983
+ }
3984
+
3985
+ .chat-input__container {
3986
+ width: 100%;
3987
+ max-width: var(--tplane-chat-max-width);
3988
+ margin: 0 auto;
3989
+ }
3990
+
3991
+ .chat-input__pill {
3992
+ display: flex;
3993
+ align-items: center;
3994
+ gap: 8px;
3995
+ background: var(--tplane-chat-surface);
3996
+ border: 1px solid var(--tplane-chat-separator);
3997
+ border-radius: 9999px;
3998
+ padding: 8px 8px 8px 16px;
3999
+ min-height: 56px;
4000
+ box-sizing: border-box;
4001
+ }
4002
+
4003
+ .chat-input__textarea {
4004
+ flex: 1 1 auto;
4005
+ border: 0;
4006
+ outline: none;
4007
+ resize: none;
4008
+ background: transparent;
4009
+ color: var(--tplane-chat-text);
4010
+ font: inherit;
4011
+ font-size: 1rem;
4012
+ line-height: 1.5;
4013
+ padding: 0;
4014
+ field-sizing: content;
4015
+ overflow-y: auto;
4016
+ }
4017
+ .chat-input__textarea::placeholder { color: var(--tplane-chat-text-muted); }
4018
+ .chat-input__textarea::-webkit-scrollbar { width: 4px; }
4019
+ .chat-input__textarea::-webkit-scrollbar-thumb { background: var(--tplane-chat-separator); border-radius: 4px; }
4020
+
4021
+ .chat-input__controls {
4022
+ display: flex;
4023
+ align-items: center;
4024
+ gap: 4px;
4025
+ flex: none;
4026
+ }
4027
+
4028
+ .chat-input__send,
4029
+ .chat-input__send--stop {
4030
+ width: 36px;
4031
+ height: 36px;
4032
+ border-radius: 50%;
4033
+ border: 0;
4034
+ display: flex;
4035
+ align-items: center;
4036
+ justify-content: center;
4037
+ cursor: pointer;
4038
+ transition: opacity 150ms ease, transform 150ms ease, background 150ms ease;
4039
+ padding: 0;
4040
+ }
4041
+ .chat-input__send {
4042
+ background: var(--tplane-chat-text);
4043
+ color: var(--tplane-chat-bg);
4044
+ }
4045
+ .chat-input__send:disabled {
4046
+ opacity: 0.35;
4047
+ cursor: not-allowed;
4048
+ background: var(--tplane-chat-text-muted);
4049
+ }
4050
+ .chat-input__send:not(:disabled):hover { transform: scale(1.05); }
4051
+ .chat-input__send svg { width: 16px; height: 16px; }
4052
+
4053
+ .chat-input__send--stop {
4054
+ background: var(--tplane-chat-text-muted);
4055
+ color: var(--tplane-chat-bg);
4056
+ }
4057
+ .chat-input__send--stop:hover { transform: scale(1.05); }
4058
+ .chat-input__send--stop svg { width: 14px; height: 14px; }
4059
+ `;
4060
+
4061
+ // libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
4062
+ // SPDX-License-Identifier: MIT
4063
+ /**
4064
+ * Submits a trimmed message to the agent.
4065
+ * Returns the trimmed string on success, or `null` if the input was empty.
4066
+ */
4067
+ function submitMessage(agent, text) {
4068
+ const trimmed = text.trim();
4069
+ if (!trimmed)
4070
+ return null;
4071
+ void agent.submit({ message: trimmed });
4072
+ return trimmed;
4073
+ }
4074
+ class ChatInputComponent {
4075
+ agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
4076
+ submitOnEnter = input(true, ...(ngDevMode ? [{ debugName: "submitOnEnter" }] : []));
4077
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
4078
+ /** When true (default), shows a stop button while the agent is streaming. */
4079
+ showStopButton = input(true, ...(ngDevMode ? [{ debugName: "showStopButton" }] : []));
4080
+ submitted = output();
4081
+ stopped = output();
4082
+ messageText = signal('', ...(ngDevMode ? [{ debugName: "messageText" }] : []));
4083
+ isLoading = computed(() => this.agent().isLoading(), ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
4084
+ /** True while an IME composition (CJK input, accent, autocorrect) is active. */
4085
+ composing = signal(false, ...(ngDevMode ? [{ debugName: "composing" }] : []));
4086
+ focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : []));
4087
+ /** Submit is allowed only when not loading and there's non-whitespace text. */
4088
+ canSubmit = computed(() => {
4089
+ if (this.isLoading())
4090
+ return false;
4091
+ return this.messageText().trim().length > 0;
4092
+ }, ...(ngDevMode ? [{ debugName: "canSubmit" }] : []));
4093
+ /** The stop button only appears when the consumer opted in AND we're loading. */
4094
+ canStop = computed(() => this.showStopButton(), ...(ngDevMode ? [{ debugName: "canStop" }] : []));
4095
+ textareaEl = viewChild('textareaEl', ...(ngDevMode ? [{ debugName: "textareaEl" }] : []));
4096
+ /**
4097
+ * Auto-resize the textarea to fit its content as the user types or pastes
4098
+ * multi-line text. Caps at min(40vh, 320px); beyond that the textarea
4099
+ * scrolls. Without this, multi-line input is hidden behind the rows="1"
4100
+ * fixed height (caught by live browser smoke).
4101
+ */
3576
4102
  constructor() {
3577
4103
  effect(() => {
3578
- if (this.open()) {
3579
- this.activeIndex.set(0);
3580
- queueMicrotask(() => this.inputEl()?.nativeElement.focus());
3581
- }
3582
- });
3583
- effect(() => {
3584
- const max = this.results().length - 1;
3585
- if (max >= 0 && this.activeIndex() > max) {
3586
- this.activeIndex.set(max);
3587
- }
4104
+ const text = this.messageText();
4105
+ const el = this.textareaEl()?.nativeElement;
4106
+ if (!el)
4107
+ return;
4108
+ // Cap: min(40vh, 320px). Recomputed on each input so viewport resizes
4109
+ // between keystrokes are picked up without a dedicated resize listener.
4110
+ const viewportH = typeof window === 'undefined' ? 600 : window.innerHeight;
4111
+ const cap = Math.min(viewportH * 0.4, 320);
4112
+ el.style.height = 'auto';
4113
+ const next = Math.min(el.scrollHeight, cap);
4114
+ el.style.height = `${next}px`;
4115
+ el.style.overflowY = el.scrollHeight > cap ? 'auto' : 'hidden';
4116
+ void text;
3588
4117
  });
3589
4118
  }
3590
- rowId(index) {
3591
- return `${this.listId}__row-${index}`;
4119
+ focusTextarea() {
4120
+ this.textareaEl()?.nativeElement.focus();
3592
4121
  }
3593
- activeRowId() {
3594
- return this.results().length > 0 ? this.rowId(this.activeIndex()) : null;
4122
+ onSubmit() {
4123
+ const submitted = submitMessage(this.agent(), this.messageText());
4124
+ if (submitted !== null) {
4125
+ this.submitted.emit(submitted);
4126
+ this.messageText.set('');
4127
+ const el = this.textareaEl()?.nativeElement;
4128
+ if (el)
4129
+ el.value = '';
4130
+ requestAnimationFrame(() => this.textareaEl()?.nativeElement.focus());
4131
+ }
3595
4132
  }
3596
- onInput(e) {
3597
- const value = e.target.value;
3598
- this.query.set(value);
4133
+ /** Sync the textarea's value into the signal on user input. A direct
4134
+ * [value]/(input) pair is used instead of ngModel: NgModel does not
4135
+ * reliably write a programmatic clear back to the view under zoneless
4136
+ * + OnPush, leaving sent text visible in the composer (audit F1). */
4137
+ onInput(event) {
4138
+ this.messageText.set(event.target.value);
3599
4139
  }
3600
- onInputKeydown(e) {
3601
- if (e.key === 'Escape') {
3602
- e.preventDefault();
3603
- this.closed.emit();
3604
- return;
4140
+ /** Abort the current streaming response (if the adapter supports it). */
4141
+ onStop() {
4142
+ const a = this.agent();
4143
+ if (typeof a.stop === 'function') {
4144
+ void a.stop();
3605
4145
  }
3606
- if (e.key === 'ArrowDown') {
3607
- e.preventDefault();
3608
- const max = this.results().length - 1;
3609
- if (max < 0)
3610
- return;
3611
- this.activeIndex.set(Math.min(this.activeIndex() + 1, max));
4146
+ this.stopped.emit();
4147
+ }
4148
+ onKeydown(event) {
4149
+ if (!this.submitOnEnter() || event.shiftKey)
3612
4150
  return;
3613
- }
3614
- if (e.key === 'ArrowUp') {
3615
- e.preventDefault();
3616
- this.activeIndex.set(Math.max(this.activeIndex() - 1, 0));
4151
+ // Don't submit while an IME composition is in progress (CJK input,
4152
+ // dead-key accents, autocorrect popups). The composition's terminating
4153
+ // Enter must reach the textarea so the candidate is committed; submitting
4154
+ // here would discard the user's in-progress character.
4155
+ if (this.composing() || event.isComposing || event.keyCode === 229)
3617
4156
  return;
3618
- }
3619
- if (e.key === 'Enter') {
3620
- e.preventDefault();
3621
- const rows = this.results();
3622
- if (rows.length === 0)
3623
- return;
3624
- const row = rows[this.activeIndex()];
3625
- this.threadSelected.emit(row.id);
3626
- return;
3627
- }
3628
- }
3629
- onRowClick(id) {
3630
- this.threadSelected.emit(id);
4157
+ event.preventDefault();
4158
+ this.onSubmit();
3631
4159
  }
3632
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatHistorySearchPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3633
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatHistorySearchPaletteComponent, isStandalone: true, selector: "chat-history-search-palette", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, results: { classPropertyName: "results", publicName: "results", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", query: "queryChange", threadSelected: "threadSelected", closed: "closed" }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
3634
- @if (open()) {
3635
- <button
3636
- type="button"
3637
- class="chat-history-search-palette__scrim"
3638
- aria-label="Close search"
3639
- (click)="closed.emit()"
3640
- ></button>
3641
- <div
3642
- class="chat-history-search-palette"
3643
- role="dialog"
3644
- aria-modal="true"
3645
- aria-label="Search conversations"
3646
- >
3647
- <div class="chat-history-search-palette__input-row">
3648
- <svg class="chat-history-search-palette__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3649
- <circle cx="11" cy="11" r="7"/>
3650
- <line x1="21" y1="21" x2="16.65" y2="16.65"/>
3651
- </svg>
3652
- <input
3653
- #inputEl
3654
- class="chat-history-search-palette__input"
3655
- type="text"
3656
- role="combobox"
3657
- aria-expanded="true"
3658
- [attr.aria-controls]="listId"
3659
- [attr.aria-activedescendant]="activeRowId()"
3660
- [placeholder]="placeholder()"
3661
- [value]="query()"
3662
- (input)="onInput($event)"
3663
- (keydown)="onInputKeydown($event)"
3664
- />
3665
- <button
3666
- type="button"
3667
- class="chat-history-search-palette__close"
3668
- aria-label="Close"
3669
- (click)="closed.emit()"
3670
- >
3671
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3672
- <line x1="18" y1="6" x2="6" y2="18"/>
3673
- <line x1="6" y1="6" x2="18" y2="18"/>
3674
- </svg>
3675
- </button>
4160
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4161
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatInputComponent, isStandalone: true, selector: "chat-input", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, submitOnEnter: { classPropertyName: "submitOnEnter", publicName: "submitOnEnter", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showStopButton: { classPropertyName: "showStopButton", publicName: "showStopButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", stopped: "stopped" }, viewQueries: [{ propertyName: "textareaEl", first: true, predicate: ["textareaEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
4162
+ <div class="chat-input__container">
4163
+ <ng-content select="[chatInputBanner]" />
4164
+ <ng-content select="[chatInputAttachments]" />
4165
+ <div class="chat-input__pill">
4166
+ <ng-content select="[chatInputLeading]" />
4167
+ <textarea
4168
+ #textareaEl
4169
+ class="chat-input__textarea"
4170
+ [value]="messageText()"
4171
+ (input)="onInput($event)"
4172
+ [placeholder]="placeholder()"
4173
+ (keydown.enter)="onKeydown($any($event))"
4174
+ (compositionstart)="composing.set(true)"
4175
+ (compositionend)="composing.set(false)"
4176
+ (focus)="focused.set(true)"
4177
+ (blur)="focused.set(false)"
4178
+ name="messageText"
4179
+ rows="1"
4180
+ aria-label="Type a message"
4181
+ ></textarea>
4182
+ <div class="chat-input__controls">
4183
+ <ng-content select="[chatInputModelSelect]" />
4184
+ <ng-content select="[chatInputTrailing]" />
4185
+ @if (isLoading() && canStop()) {
4186
+ <button
4187
+ type="button"
4188
+ class="chat-input__send chat-input__send--stop"
4189
+ (click)="onStop()"
4190
+ aria-label="Stop generating"
4191
+ title="Stop generating"
4192
+ >
4193
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
4194
+ <rect x="6" y="6" width="12" height="12" rx="2"/>
4195
+ </svg>
4196
+ </button>
4197
+ } @else {
4198
+ <button
4199
+ type="button"
4200
+ class="chat-input__send"
4201
+ [disabled]="!canSubmit()"
4202
+ (click)="onSubmit()"
4203
+ aria-label="Send message"
4204
+ >
4205
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4206
+ <line x1="12" y1="19" x2="12" y2="5"/>
4207
+ <polyline points="5 12 12 5 19 12"/>
4208
+ </svg>
4209
+ </button>
4210
+ }
3676
4211
  </div>
3677
-
3678
- @if (loading() && results().length === 0) {
3679
- <div class="chat-history-search-palette__skeleton" aria-hidden="true">
3680
- <div class="chat-history-search-palette__skeleton-row"></div>
3681
- <div class="chat-history-search-palette__skeleton-row"></div>
3682
- <div class="chat-history-search-palette__skeleton-row"></div>
3683
- </div>
3684
- } @else if (results().length === 0 && query().length === 0) {
3685
- <div class="chat-history-search-palette__hint">Type to search your conversations.</div>
3686
- } @else if (results().length === 0) {
3687
- <div class="chat-history-search-palette__empty">No conversations match.</div>
3688
- } @else {
3689
- <ul class="chat-history-search-palette__list" role="listbox" [id]="listId">
3690
- @for (row of results(); let i = $index; track row.id) {
3691
- <li
3692
- class="chat-history-search-palette__row"
3693
- role="option"
3694
- tabindex="-1"
3695
- [id]="rowId(i)"
3696
- [attr.aria-selected]="i === activeIndex() ? 'true' : 'false'"
3697
- (click)="onRowClick(row.id)"
3698
- (keydown.enter)="onRowClick(row.id)"
3699
- (keydown.space)="onRowClick(row.id)"
3700
- >
3701
- <span class="chat-history-search-palette__row-title">{{ row.title }}</span>
3702
- @if (row.subtitle) {
3703
- <span class="chat-history-search-palette__row-subtitle">{{ row.subtitle }}</span>
3704
- }
3705
- </li>
3706
- }
3707
- </ul>
3708
- }
3709
4212
  </div>
3710
- }
3711
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-history-search-palette__scrim{position:fixed;inset:0;background:#0006;z-index:var(--tplane-chat-z-modal-scrim, 1100);border:0;padding:0;cursor:pointer}.chat-history-search-palette{position:fixed;top:15vh;left:50%;transform:translate(-50%);width:min(560px,90vw);max-height:70vh;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:12px;box-shadow:0 16px 48px #00000040;z-index:var(--tplane-chat-z-modal, 1101);display:flex;flex-direction:column;overflow:hidden}.chat-history-search-palette__input-row{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--tplane-chat-separator)}.chat-history-search-palette__icon{width:18px;height:18px;color:var(--tplane-chat-text-muted);flex-shrink:0}.chat-history-search-palette__input{flex:1 1 auto;border:0;outline:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem}.chat-history-search-palette__input::placeholder{color:var(--tplane-chat-text-muted)}.chat-history-search-palette__close{background:transparent;border:0;padding:4px;color:var(--tplane-chat-text-muted);cursor:pointer;border-radius:4px}.chat-history-search-palette__close:hover{color:var(--tplane-chat-text)}.chat-history-search-palette__list{flex:1 1 auto;overflow-y:auto;padding:4px;margin:0;list-style:none}.chat-history-search-palette__row{display:flex;flex-direction:column;padding:10px 12px;border-radius:8px;cursor:pointer}.chat-history-search-palette__row[aria-selected=true]{background:var(--tplane-chat-surface-alt)}.chat-history-search-palette__row-title{color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size)}.chat-history-search-palette__row-subtitle{color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-sm);margin-top:2px}.chat-history-search-palette__empty,.chat-history-search-palette__hint{padding:24px 16px;color:var(--tplane-chat-text-muted);text-align:center;font-size:var(--tplane-chat-font-size-sm)}.chat-history-search-palette__skeleton{padding:8px 4px}.chat-history-search-palette__skeleton-row{height:36px;margin:4px 0;background:var(--tplane-chat-surface-alt);border-radius:8px;animation:tplane-chat-pulse 1.4s ease-in-out infinite}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4213
+ <ng-content select="[chatInputFooter]" />
4214
+ </div>
4215
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;width:100%;padding:0 var(--tplane-chat-edge-pad);box-sizing:border-box}.chat-input__container{width:100%;max-width:var(--tplane-chat-max-width);margin:0 auto}.chat-input__pill{display:flex;align-items:center;gap:8px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:9999px;padding:8px 8px 8px 16px;min-height:56px;box-sizing:border-box}.chat-input__textarea{flex:1 1 auto;border:0;outline:none;resize:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem;line-height:1.5;padding:0;field-sizing:content;overflow-y:auto}.chat-input__textarea::placeholder{color:var(--tplane-chat-text-muted)}.chat-input__textarea::-webkit-scrollbar{width:4px}.chat-input__textarea::-webkit-scrollbar-thumb{background:var(--tplane-chat-separator);border-radius:4px}.chat-input__controls{display:flex;align-items:center;gap:4px;flex:none}.chat-input__send,.chat-input__send--stop{width:36px;height:36px;border-radius:50%;border:0;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:opacity .15s ease,transform .15s ease,background .15s ease;padding:0}.chat-input__send{background:var(--tplane-chat-text);color:var(--tplane-chat-bg)}.chat-input__send:disabled{opacity:.35;cursor:not-allowed;background:var(--tplane-chat-text-muted)}.chat-input__send:not(:disabled):hover{transform:scale(1.05)}.chat-input__send svg{width:16px;height:16px}.chat-input__send--stop{background:var(--tplane-chat-text-muted);color:var(--tplane-chat-bg)}.chat-input__send--stop:hover{transform:scale(1.05)}.chat-input__send--stop svg{width:14px;height:14px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3712
4216
  }
3713
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatHistorySearchPaletteComponent, decorators: [{
4217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatInputComponent, decorators: [{
3714
4218
  type: Component,
3715
- args: [{ selector: 'chat-history-search-palette', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3716
- @if (open()) {
3717
- <button
3718
- type="button"
3719
- class="chat-history-search-palette__scrim"
3720
- aria-label="Close search"
3721
- (click)="closed.emit()"
3722
- ></button>
3723
- <div
3724
- class="chat-history-search-palette"
3725
- role="dialog"
3726
- aria-modal="true"
3727
- aria-label="Search conversations"
3728
- >
3729
- <div class="chat-history-search-palette__input-row">
3730
- <svg class="chat-history-search-palette__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3731
- <circle cx="11" cy="11" r="7"/>
3732
- <line x1="21" y1="21" x2="16.65" y2="16.65"/>
3733
- </svg>
3734
- <input
3735
- #inputEl
3736
- class="chat-history-search-palette__input"
3737
- type="text"
3738
- role="combobox"
3739
- aria-expanded="true"
3740
- [attr.aria-controls]="listId"
3741
- [attr.aria-activedescendant]="activeRowId()"
3742
- [placeholder]="placeholder()"
3743
- [value]="query()"
3744
- (input)="onInput($event)"
3745
- (keydown)="onInputKeydown($event)"
3746
- />
3747
- <button
3748
- type="button"
3749
- class="chat-history-search-palette__close"
3750
- aria-label="Close"
3751
- (click)="closed.emit()"
3752
- >
3753
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3754
- <line x1="18" y1="6" x2="6" y2="18"/>
3755
- <line x1="6" y1="6" x2="18" y2="18"/>
3756
- </svg>
3757
- </button>
4219
+ args: [{ selector: 'chat-input', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4220
+ <div class="chat-input__container">
4221
+ <ng-content select="[chatInputBanner]" />
4222
+ <ng-content select="[chatInputAttachments]" />
4223
+ <div class="chat-input__pill">
4224
+ <ng-content select="[chatInputLeading]" />
4225
+ <textarea
4226
+ #textareaEl
4227
+ class="chat-input__textarea"
4228
+ [value]="messageText()"
4229
+ (input)="onInput($event)"
4230
+ [placeholder]="placeholder()"
4231
+ (keydown.enter)="onKeydown($any($event))"
4232
+ (compositionstart)="composing.set(true)"
4233
+ (compositionend)="composing.set(false)"
4234
+ (focus)="focused.set(true)"
4235
+ (blur)="focused.set(false)"
4236
+ name="messageText"
4237
+ rows="1"
4238
+ aria-label="Type a message"
4239
+ ></textarea>
4240
+ <div class="chat-input__controls">
4241
+ <ng-content select="[chatInputModelSelect]" />
4242
+ <ng-content select="[chatInputTrailing]" />
4243
+ @if (isLoading() && canStop()) {
4244
+ <button
4245
+ type="button"
4246
+ class="chat-input__send chat-input__send--stop"
4247
+ (click)="onStop()"
4248
+ aria-label="Stop generating"
4249
+ title="Stop generating"
4250
+ >
4251
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
4252
+ <rect x="6" y="6" width="12" height="12" rx="2"/>
4253
+ </svg>
4254
+ </button>
4255
+ } @else {
4256
+ <button
4257
+ type="button"
4258
+ class="chat-input__send"
4259
+ [disabled]="!canSubmit()"
4260
+ (click)="onSubmit()"
4261
+ aria-label="Send message"
4262
+ >
4263
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4264
+ <line x1="12" y1="19" x2="12" y2="5"/>
4265
+ <polyline points="5 12 12 5 19 12"/>
4266
+ </svg>
4267
+ </button>
4268
+ }
3758
4269
  </div>
4270
+ </div>
4271
+ <ng-content select="[chatInputFooter]" />
4272
+ </div>
4273
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;width:100%;padding:0 var(--tplane-chat-edge-pad);box-sizing:border-box}.chat-input__container{width:100%;max-width:var(--tplane-chat-max-width);margin:0 auto}.chat-input__pill{display:flex;align-items:center;gap:8px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:9999px;padding:8px 8px 8px 16px;min-height:56px;box-sizing:border-box}.chat-input__textarea{flex:1 1 auto;border:0;outline:none;resize:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem;line-height:1.5;padding:0;field-sizing:content;overflow-y:auto}.chat-input__textarea::placeholder{color:var(--tplane-chat-text-muted)}.chat-input__textarea::-webkit-scrollbar{width:4px}.chat-input__textarea::-webkit-scrollbar-thumb{background:var(--tplane-chat-separator);border-radius:4px}.chat-input__controls{display:flex;align-items:center;gap:4px;flex:none}.chat-input__send,.chat-input__send--stop{width:36px;height:36px;border-radius:50%;border:0;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:opacity .15s ease,transform .15s ease,background .15s ease;padding:0}.chat-input__send{background:var(--tplane-chat-text);color:var(--tplane-chat-bg)}.chat-input__send:disabled{opacity:.35;cursor:not-allowed;background:var(--tplane-chat-text-muted)}.chat-input__send:not(:disabled):hover{transform:scale(1.05)}.chat-input__send svg{width:16px;height:16px}.chat-input__send--stop{background:var(--tplane-chat-text-muted);color:var(--tplane-chat-bg)}.chat-input__send--stop:hover{transform:scale(1.05)}.chat-input__send--stop svg{width:14px;height:14px}\n"] }]
4274
+ }], ctorParameters: () => [], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }], submitOnEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnEnter", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showStopButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showStopButton", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], stopped: [{ type: i0.Output, args: ["stopped"] }], textareaEl: [{ type: i0.ViewChild, args: ['textareaEl', { isSignal: true }] }] } });
3759
4275
 
3760
- @if (loading() && results().length === 0) {
3761
- <div class="chat-history-search-palette__skeleton" aria-hidden="true">
3762
- <div class="chat-history-search-palette__skeleton-row"></div>
3763
- <div class="chat-history-search-palette__skeleton-row"></div>
3764
- <div class="chat-history-search-palette__skeleton-row"></div>
3765
- </div>
3766
- } @else if (results().length === 0 && query().length === 0) {
3767
- <div class="chat-history-search-palette__hint">Type to search your conversations.</div>
3768
- } @else if (results().length === 0) {
3769
- <div class="chat-history-search-palette__empty">No conversations match.</div>
3770
- } @else {
3771
- <ul class="chat-history-search-palette__list" role="listbox" [id]="listId">
3772
- @for (row of results(); let i = $index; track row.id) {
3773
- <li
3774
- class="chat-history-search-palette__row"
3775
- role="option"
3776
- tabindex="-1"
3777
- [id]="rowId(i)"
3778
- [attr.aria-selected]="i === activeIndex() ? 'true' : 'false'"
3779
- (click)="onRowClick(row.id)"
3780
- (keydown.enter)="onRowClick(row.id)"
3781
- (keydown.space)="onRowClick(row.id)"
3782
- >
3783
- <span class="chat-history-search-palette__row-title">{{ row.title }}</span>
3784
- @if (row.subtitle) {
3785
- <span class="chat-history-search-palette__row-subtitle">{{ row.subtitle }}</span>
3786
- }
3787
- </li>
3788
- }
3789
- </ul>
3790
- }
4276
+ // SPDX-License-Identifier: MIT
4277
+ const CHAT_TYPING_INDICATOR_STYLES = `
4278
+ /* Sit in the same centered column as chat-message-list so the dots
4279
+ don't flash at the scroll container's left edge before the assistant
4280
+ message renders. */
4281
+ :host {
4282
+ display: block;
4283
+ padding: 0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);
4284
+ max-width: var(--tplane-chat-max-width);
4285
+ margin: 0 auto;
4286
+ width: 100%;
4287
+ box-sizing: border-box;
4288
+ }
4289
+ .chat-typing__dots { display: inline-flex; gap: 4px; align-items: center; }
4290
+ .chat-typing__dot {
4291
+ width: 6px;
4292
+ height: 6px;
4293
+ border-radius: 50%;
4294
+ background: var(--tplane-chat-text-muted);
4295
+ animation: tplane-chat-typing-dot 1.4s ease-in-out infinite both;
4296
+ }
4297
+ .chat-typing__dot:nth-child(2) { animation-delay: 0.2s; }
4298
+ .chat-typing__dot:nth-child(3) { animation-delay: 0.4s; }
4299
+ `;
4300
+
4301
+ // libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts
4302
+ // SPDX-License-Identifier: MIT
4303
+ /**
4304
+ * Whether the agent should show a "typing" indicator — it is loading and has
4305
+ * not yet started streaming the assistant's reply.
4306
+ *
4307
+ * @param agent The agent to inspect.
4308
+ * @returns `true` while the agent is awaiting a response but no assistant text
4309
+ * has streamed yet; `false` once tokens arrive or the agent is idle.
4310
+ * @example
4311
+ * ```ts
4312
+ * \@if (isTyping(agent)) { <chat-typing-indicator [agent]="agent" /> }
4313
+ * ```
4314
+ */
4315
+ function isTyping(agent) {
4316
+ if (!agent.isLoading())
4317
+ return false;
4318
+ const msgs = agent.messages();
4319
+ if (msgs.length === 0)
4320
+ return true;
4321
+ const last = msgs[msgs.length - 1];
4322
+ if (last.role === 'user')
4323
+ return true;
4324
+ if (last.role === 'assistant') {
4325
+ return typeof last.content === 'string'
4326
+ ? !last.content
4327
+ : last.content.length === 0;
4328
+ }
4329
+ return false;
4330
+ }
4331
+ class ChatTypingIndicatorComponent {
4332
+ agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
4333
+ visible = computed(() => isTyping(this.agent()), ...(ngDevMode ? [{ debugName: "visible" }] : []));
4334
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatTypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4335
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatTypingIndicatorComponent, isStandalone: true, selector: "chat-typing-indicator", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
4336
+ @if (visible()) {
4337
+ <div class="chat-typing__dots" role="status" aria-label="Assistant is typing">
4338
+ <span class="chat-typing__dot"></span>
4339
+ <span class="chat-typing__dot"></span>
4340
+ <span class="chat-typing__dot"></span>
3791
4341
  </div>
3792
4342
  }
3793
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-history-search-palette__scrim{position:fixed;inset:0;background:#0006;z-index:var(--tplane-chat-z-modal-scrim, 1100);border:0;padding:0;cursor:pointer}.chat-history-search-palette{position:fixed;top:15vh;left:50%;transform:translate(-50%);width:min(560px,90vw);max-height:70vh;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:12px;box-shadow:0 16px 48px #00000040;z-index:var(--tplane-chat-z-modal, 1101);display:flex;flex-direction:column;overflow:hidden}.chat-history-search-palette__input-row{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--tplane-chat-separator)}.chat-history-search-palette__icon{width:18px;height:18px;color:var(--tplane-chat-text-muted);flex-shrink:0}.chat-history-search-palette__input{flex:1 1 auto;border:0;outline:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem}.chat-history-search-palette__input::placeholder{color:var(--tplane-chat-text-muted)}.chat-history-search-palette__close{background:transparent;border:0;padding:4px;color:var(--tplane-chat-text-muted);cursor:pointer;border-radius:4px}.chat-history-search-palette__close:hover{color:var(--tplane-chat-text)}.chat-history-search-palette__list{flex:1 1 auto;overflow-y:auto;padding:4px;margin:0;list-style:none}.chat-history-search-palette__row{display:flex;flex-direction:column;padding:10px 12px;border-radius:8px;cursor:pointer}.chat-history-search-palette__row[aria-selected=true]{background:var(--tplane-chat-surface-alt)}.chat-history-search-palette__row-title{color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size)}.chat-history-search-palette__row-subtitle{color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-sm);margin-top:2px}.chat-history-search-palette__empty,.chat-history-search-palette__hint{padding:24px 16px;color:var(--tplane-chat-text-muted);text-align:center;font-size:var(--tplane-chat-font-size-sm)}.chat-history-search-palette__skeleton{padding:8px 4px}.chat-history-search-palette__skeleton-row{height:36px;margin:4px 0;background:var(--tplane-chat-surface-alt);border-radius:8px;animation:tplane-chat-pulse 1.4s ease-in-out infinite}\n"] }]
3794
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }], results: [{ type: i0.Input, args: [{ isSignal: true, alias: "results", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], threadSelected: [{ type: i0.Output, args: ["threadSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
4343
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;padding:0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);max-width:var(--tplane-chat-max-width);margin:0 auto;width:100%;box-sizing:border-box}.chat-typing__dots{display:inline-flex;gap:4px;align-items:center}.chat-typing__dot{width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:tplane-chat-typing-dot 1.4s ease-in-out infinite both}.chat-typing__dot:nth-child(2){animation-delay:.2s}.chat-typing__dot:nth-child(3){animation-delay:.4s}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4344
+ }
4345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatTypingIndicatorComponent, decorators: [{
4346
+ type: Component,
4347
+ args: [{ selector: 'chat-typing-indicator', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
4348
+ @if (visible()) {
4349
+ <div class="chat-typing__dots" role="status" aria-label="Assistant is typing">
4350
+ <span class="chat-typing__dot"></span>
4351
+ <span class="chat-typing__dot"></span>
4352
+ <span class="chat-typing__dot"></span>
4353
+ </div>
4354
+ }
4355
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:block;padding:0 var(--tplane-chat-space-6) var(--tplane-chat-space-3);max-width:var(--tplane-chat-max-width);margin:0 auto;width:100%;box-sizing:border-box}.chat-typing__dots{display:inline-flex;gap:4px;align-items:center}.chat-typing__dot{width:6px;height:6px;border-radius:50%;background:var(--tplane-chat-text-muted);animation:tplane-chat-typing-dot 1.4s ease-in-out infinite both}.chat-typing__dot:nth-child(2){animation-delay:.2s}.chat-typing__dot:nth-child(3){animation-delay:.4s}\n"] }]
4356
+ }], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }] } });
3795
4357
 
3796
- // libs/chat/src/lib/styles/chat-overflow-menu.styles.ts
4358
+ // libs/chat/src/lib/styles/chat-history-search-palette.styles.ts
3797
4359
  // SPDX-License-Identifier: MIT
3798
- const CHAT_OVERFLOW_MENU_STYLES = `
4360
+ const CHAT_HISTORY_SEARCH_PALETTE_STYLES = `
3799
4361
  :host { display: contents; }
3800
- .chat-overflow-menu__scrim {
4362
+ .chat-history-search-palette__scrim {
3801
4363
  position: fixed;
3802
4364
  inset: 0;
3803
- background: transparent;
3804
- z-index: 59;
4365
+ background: rgba(0, 0, 0, 0.4);
4366
+ z-index: var(--tplane-chat-z-modal-scrim, 1100);
3805
4367
  border: 0;
3806
4368
  padding: 0;
3807
- cursor: default;
4369
+ cursor: pointer;
3808
4370
  }
3809
- .chat-overflow-menu {
4371
+ .chat-history-search-palette {
3810
4372
  position: fixed;
3811
- z-index: 60;
3812
- min-width: 160px;
3813
- padding: 4px;
3814
- margin: 0;
3815
- list-style: none;
4373
+ top: 15vh;
4374
+ left: 50%;
4375
+ transform: translateX(-50%);
4376
+ width: min(560px, 90vw);
4377
+ max-height: 70vh;
3816
4378
  background: var(--tplane-chat-bg);
3817
4379
  border: 1px solid var(--tplane-chat-separator);
3818
- border-radius: 8px;
3819
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
4380
+ border-radius: 12px;
4381
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
4382
+ z-index: var(--tplane-chat-z-modal, 1101);
4383
+ display: flex;
4384
+ flex-direction: column;
4385
+ overflow: hidden;
3820
4386
  }
3821
- .chat-overflow-menu__item {
3822
- display: block;
3823
- padding: 8px 12px;
3824
- border-radius: 4px;
4387
+ .chat-history-search-palette__input-row {
4388
+ display: flex;
4389
+ align-items: center;
4390
+ gap: 8px;
4391
+ padding: 12px 16px;
4392
+ border-bottom: 1px solid var(--tplane-chat-separator);
4393
+ }
4394
+ .chat-history-search-palette__icon {
4395
+ width: 18px;
4396
+ height: 18px;
4397
+ color: var(--tplane-chat-text-muted);
4398
+ flex-shrink: 0;
4399
+ }
4400
+ .chat-history-search-palette__input {
4401
+ flex: 1 1 auto;
4402
+ border: 0;
4403
+ outline: none;
4404
+ background: transparent;
3825
4405
  color: var(--tplane-chat-text);
3826
- font-size: var(--tplane-chat-font-size-sm);
4406
+ font: inherit;
4407
+ font-size: 1rem;
4408
+ }
4409
+ .chat-history-search-palette__input::placeholder {
4410
+ color: var(--tplane-chat-text-muted);
4411
+ }
4412
+ .chat-history-search-palette__close {
4413
+ background: transparent;
4414
+ border: 0;
4415
+ padding: 4px;
4416
+ color: var(--tplane-chat-text-muted);
3827
4417
  cursor: pointer;
3828
- user-select: none;
4418
+ border-radius: 4px;
3829
4419
  }
3830
- .chat-overflow-menu__item:hover {
4420
+ .chat-history-search-palette__close:hover { color: var(--tplane-chat-text); }
4421
+ .chat-history-search-palette__list {
4422
+ flex: 1 1 auto;
4423
+ overflow-y: auto;
4424
+ padding: 4px;
4425
+ margin: 0;
4426
+ list-style: none;
4427
+ }
4428
+ .chat-history-search-palette__row {
4429
+ display: flex;
4430
+ flex-direction: column;
4431
+ padding: 10px 12px;
4432
+ border-radius: 8px;
4433
+ cursor: pointer;
4434
+ }
4435
+ .chat-history-search-palette__row[aria-selected="true"] {
3831
4436
  background: var(--tplane-chat-surface-alt);
3832
4437
  }
3833
- .chat-overflow-menu__item:focus-visible {
3834
- outline: 2px solid var(--tplane-chat-primary);
3835
- outline-offset: -2px;
4438
+ .chat-history-search-palette__row-title {
4439
+ color: var(--tplane-chat-text);
4440
+ font-size: var(--tplane-chat-font-size);
3836
4441
  }
3837
- .chat-overflow-menu__item--destructive {
3838
- color: var(--tplane-chat-error-text);
4442
+ .chat-history-search-palette__row-subtitle {
4443
+ color: var(--tplane-chat-text-muted);
4444
+ font-size: var(--tplane-chat-font-size-sm);
4445
+ margin-top: 2px;
3839
4446
  }
3840
- .chat-overflow-menu__item--disabled {
4447
+ .chat-history-search-palette__empty,
4448
+ .chat-history-search-palette__hint {
4449
+ padding: 24px 16px;
3841
4450
  color: var(--tplane-chat-text-muted);
3842
- cursor: not-allowed;
3843
- pointer-events: none;
4451
+ text-align: center;
4452
+ font-size: var(--tplane-chat-font-size-sm);
4453
+ }
4454
+ .chat-history-search-palette__skeleton {
4455
+ padding: 8px 4px;
4456
+ }
4457
+ .chat-history-search-palette__skeleton-row {
4458
+ height: 36px;
4459
+ margin: 4px 0;
4460
+ background: var(--tplane-chat-surface-alt);
4461
+ border-radius: 8px;
4462
+ animation: tplane-chat-pulse 1.4s ease-in-out infinite;
3844
4463
  }
3845
4464
  `;
3846
4465
 
3847
- // libs/chat/src/lib/primitives/chat-overflow-menu/chat-overflow-menu.component.ts
4466
+ // libs/chat/src/lib/primitives/chat-history-search-palette/chat-history-search-palette.component.ts
3848
4467
  // SPDX-License-Identifier: MIT
3849
- class ChatOverflowMenuComponent {
3850
- open = input(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
3851
- items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3852
- /** Element the menu anchors against (positions just below its bottom-right corner). */
3853
- anchor = input(null, ...(ngDevMode ? [{ debugName: "anchor" }] : []));
3854
- /** Alternative anchor: explicit viewport coordinates (e.g. cursor position
3855
- * from a right-click). Takes precedence over `anchor` when set. */
3856
- anchorPos = input(null, ...(ngDevMode ? [{ debugName: "anchorPos" }] : []));
3857
- itemSelected = output();
4468
+ let paletteInstanceCounter = 0;
4469
+ class ChatHistorySearchPaletteComponent {
4470
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4471
+ query = model('', ...(ngDevMode ? [{ debugName: "query" }] : []));
4472
+ results = input([], ...(ngDevMode ? [{ debugName: "results" }] : []));
4473
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
4474
+ placeholder = input('Search conversations', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
4475
+ threadSelected = output();
3858
4476
  closed = output();
3859
- position = computed(() => {
3860
- if (!this.open())
3861
- return { top: 0, left: 0 };
3862
- const pos = this.anchorPos();
3863
- if (pos) {
3864
- return { top: pos.y + 4, left: Math.max(pos.x, 8) };
3865
- }
3866
- const el = this.anchor();
3867
- if (!el) {
3868
- const vw = typeof window === 'undefined' ? 0 : window.innerWidth;
3869
- const vh = typeof window === 'undefined' ? 0 : window.innerHeight;
3870
- return { top: Math.max(vh / 3, 0), left: Math.max(vw / 2 - 80, 0) };
3871
- }
3872
- const rect = el.getBoundingClientRect();
3873
- return { top: rect.bottom + 4, left: Math.max(rect.right - 160, 8) };
3874
- }, ...(ngDevMode ? [{ debugName: "position" }] : []));
4477
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : []));
4478
+ listId = `chat-history-search-palette__results-${++paletteInstanceCounter}`;
4479
+ inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
3875
4480
  constructor() {
3876
4481
  effect(() => {
3877
- if (!this.open())
3878
- return;
3879
- queueMicrotask(() => {
3880
- const root = document.querySelector('.chat-overflow-menu');
3881
- const first = root?.querySelector('.chat-overflow-menu__item:not(.chat-overflow-menu__item--disabled)');
3882
- first?.focus();
3883
- });
4482
+ if (this.open()) {
4483
+ this.activeIndex.set(0);
4484
+ queueMicrotask(() => this.inputEl()?.nativeElement.focus());
4485
+ }
4486
+ });
4487
+ effect(() => {
4488
+ const max = this.results().length - 1;
4489
+ if (max >= 0 && this.activeIndex() > max) {
4490
+ this.activeIndex.set(max);
4491
+ }
3884
4492
  });
3885
4493
  }
3886
- onItemClick(item) {
3887
- if (item.disabled)
3888
- return;
3889
- this.itemSelected.emit(item.id);
3890
- this.closed.emit();
4494
+ rowId(index) {
4495
+ return `${this.listId}__row-${index}`;
3891
4496
  }
3892
- onMenuKeydown(e) {
4497
+ activeRowId() {
4498
+ return this.results().length > 0 ? this.rowId(this.activeIndex()) : null;
4499
+ }
4500
+ onInput(e) {
4501
+ const value = e.target.value;
4502
+ this.query.set(value);
4503
+ }
4504
+ onInputKeydown(e) {
3893
4505
  if (e.key === 'Escape') {
3894
4506
  e.preventDefault();
3895
4507
  this.closed.emit();
3896
4508
  return;
3897
4509
  }
3898
- if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
4510
+ if (e.key === 'ArrowDown') {
3899
4511
  e.preventDefault();
3900
- const root = e.currentTarget;
3901
- const items = Array.from(root.querySelectorAll('.chat-overflow-menu__item:not(.chat-overflow-menu__item--disabled)'));
3902
- if (items.length === 0)
4512
+ const max = this.results().length - 1;
4513
+ if (max < 0)
3903
4514
  return;
3904
- const current = document.activeElement;
3905
- const idx = current ? items.indexOf(current) : -1;
3906
- const next = e.key === 'ArrowDown'
3907
- ? Math.min((idx < 0 ? 0 : idx + 1), items.length - 1)
3908
- : Math.max(idx - 1, 0);
3909
- items[next]?.focus();
4515
+ this.activeIndex.set(Math.min(this.activeIndex() + 1, max));
4516
+ return;
4517
+ }
4518
+ if (e.key === 'ArrowUp') {
4519
+ e.preventDefault();
4520
+ this.activeIndex.set(Math.max(this.activeIndex() - 1, 0));
4521
+ return;
4522
+ }
4523
+ if (e.key === 'Enter') {
4524
+ e.preventDefault();
4525
+ const rows = this.results();
4526
+ if (rows.length === 0)
4527
+ return;
4528
+ const row = rows[this.activeIndex()];
4529
+ this.threadSelected.emit(row.id);
4530
+ return;
3910
4531
  }
3911
4532
  }
3912
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverflowMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3913
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatOverflowMenuComponent, isStandalone: true, selector: "chat-overflow-menu", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, anchorPos: { classPropertyName: "anchorPos", publicName: "anchorPos", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelected: "itemSelected", closed: "closed" }, ngImport: i0, template: `
4533
+ onRowClick(id) {
4534
+ this.threadSelected.emit(id);
4535
+ }
4536
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatHistorySearchPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4537
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatHistorySearchPaletteComponent, isStandalone: true, selector: "chat-history-search-palette", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, results: { classPropertyName: "results", publicName: "results", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", query: "queryChange", threadSelected: "threadSelected", closed: "closed" }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
3914
4538
  @if (open()) {
3915
4539
  <button
3916
4540
  type="button"
3917
- class="chat-overflow-menu__scrim"
3918
- aria-label="Close menu"
4541
+ class="chat-history-search-palette__scrim"
4542
+ aria-label="Close search"
3919
4543
  (click)="closed.emit()"
3920
4544
  ></button>
3921
- <ul
3922
- class="chat-overflow-menu"
3923
- role="menu"
3924
- tabindex="-1"
3925
- [style.top.px]="position().top"
3926
- [style.left.px]="position().left"
3927
- (keydown)="onMenuKeydown($event)"
4545
+ <div
4546
+ class="chat-history-search-palette"
4547
+ role="dialog"
4548
+ aria-modal="true"
4549
+ aria-label="Search conversations"
3928
4550
  >
3929
- @for (item of items(); track item.id) {
3930
- <li
3931
- role="menuitem"
3932
- tabindex="0"
3933
- class="chat-overflow-menu__item"
3934
- [class.chat-overflow-menu__item--destructive]="item.tone === 'destructive'"
3935
- [class.chat-overflow-menu__item--disabled]="item.disabled"
3936
- [attr.aria-disabled]="item.disabled ? 'true' : null"
3937
- [attr.tabindex]="item.disabled ? -1 : 0"
3938
- (click)="onItemClick(item)"
3939
- (keydown.enter)="onItemClick(item)"
3940
- (keydown.space)="onItemClick(item)"
4551
+ <div class="chat-history-search-palette__input-row">
4552
+ <svg class="chat-history-search-palette__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4553
+ <circle cx="11" cy="11" r="7"/>
4554
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
4555
+ </svg>
4556
+ <input
4557
+ #inputEl
4558
+ class="chat-history-search-palette__input"
4559
+ type="text"
4560
+ role="combobox"
4561
+ aria-expanded="true"
4562
+ [attr.aria-controls]="listId"
4563
+ [attr.aria-activedescendant]="activeRowId()"
4564
+ [placeholder]="placeholder()"
4565
+ [value]="query()"
4566
+ (input)="onInput($event)"
4567
+ (keydown)="onInputKeydown($event)"
4568
+ />
4569
+ <button
4570
+ type="button"
4571
+ class="chat-history-search-palette__close"
4572
+ aria-label="Close"
4573
+ (click)="closed.emit()"
3941
4574
  >
3942
- {{ item.label }}
3943
- </li>
4575
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4576
+ <line x1="18" y1="6" x2="6" y2="18"/>
4577
+ <line x1="6" y1="6" x2="18" y2="18"/>
4578
+ </svg>
4579
+ </button>
4580
+ </div>
4581
+
4582
+ @if (loading() && results().length === 0) {
4583
+ <div class="chat-history-search-palette__skeleton" aria-hidden="true">
4584
+ <div class="chat-history-search-palette__skeleton-row"></div>
4585
+ <div class="chat-history-search-palette__skeleton-row"></div>
4586
+ <div class="chat-history-search-palette__skeleton-row"></div>
4587
+ </div>
4588
+ } @else if (results().length === 0 && query().length === 0) {
4589
+ <div class="chat-history-search-palette__hint">Type to search your conversations.</div>
4590
+ } @else if (results().length === 0) {
4591
+ <div class="chat-history-search-palette__empty">No conversations match.</div>
4592
+ } @else {
4593
+ <ul class="chat-history-search-palette__list" role="listbox" [id]="listId">
4594
+ @for (row of results(); let i = $index; track row.id) {
4595
+ <li
4596
+ class="chat-history-search-palette__row"
4597
+ role="option"
4598
+ tabindex="-1"
4599
+ [id]="rowId(i)"
4600
+ [attr.aria-selected]="i === activeIndex() ? 'true' : 'false'"
4601
+ (click)="onRowClick(row.id)"
4602
+ (keydown.enter)="onRowClick(row.id)"
4603
+ (keydown.space)="onRowClick(row.id)"
4604
+ >
4605
+ <span class="chat-history-search-palette__row-title">{{ row.title }}</span>
4606
+ @if (row.subtitle) {
4607
+ <span class="chat-history-search-palette__row-subtitle">{{ row.subtitle }}</span>
4608
+ }
4609
+ </li>
4610
+ }
4611
+ </ul>
3944
4612
  }
3945
- </ul>
4613
+ </div>
3946
4614
  }
3947
- `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-overflow-menu__scrim{position:fixed;inset:0;background:transparent;z-index:59;border:0;padding:0;cursor:default}.chat-overflow-menu{position:fixed;z-index:60;min-width:160px;padding:4px;margin:0;list-style:none;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:8px;box-shadow:0 8px 24px #00000026}.chat-overflow-menu__item{display:block;padding:8px 12px;border-radius:4px;color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.chat-overflow-menu__item:hover{background:var(--tplane-chat-surface-alt)}.chat-overflow-menu__item:focus-visible{outline:2px solid var(--tplane-chat-primary);outline-offset:-2px}.chat-overflow-menu__item--destructive{color:var(--tplane-chat-error-text)}.chat-overflow-menu__item--disabled{color:var(--tplane-chat-text-muted);cursor:not-allowed;pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4615
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-history-search-palette__scrim{position:fixed;inset:0;background:#0006;z-index:var(--tplane-chat-z-modal-scrim, 1100);border:0;padding:0;cursor:pointer}.chat-history-search-palette{position:fixed;top:15vh;left:50%;transform:translate(-50%);width:min(560px,90vw);max-height:70vh;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:12px;box-shadow:0 16px 48px #00000040;z-index:var(--tplane-chat-z-modal, 1101);display:flex;flex-direction:column;overflow:hidden}.chat-history-search-palette__input-row{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--tplane-chat-separator)}.chat-history-search-palette__icon{width:18px;height:18px;color:var(--tplane-chat-text-muted);flex-shrink:0}.chat-history-search-palette__input{flex:1 1 auto;border:0;outline:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem}.chat-history-search-palette__input::placeholder{color:var(--tplane-chat-text-muted)}.chat-history-search-palette__close{background:transparent;border:0;padding:4px;color:var(--tplane-chat-text-muted);cursor:pointer;border-radius:4px}.chat-history-search-palette__close:hover{color:var(--tplane-chat-text)}.chat-history-search-palette__list{flex:1 1 auto;overflow-y:auto;padding:4px;margin:0;list-style:none}.chat-history-search-palette__row{display:flex;flex-direction:column;padding:10px 12px;border-radius:8px;cursor:pointer}.chat-history-search-palette__row[aria-selected=true]{background:var(--tplane-chat-surface-alt)}.chat-history-search-palette__row-title{color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size)}.chat-history-search-palette__row-subtitle{color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-sm);margin-top:2px}.chat-history-search-palette__empty,.chat-history-search-palette__hint{padding:24px 16px;color:var(--tplane-chat-text-muted);text-align:center;font-size:var(--tplane-chat-font-size-sm)}.chat-history-search-palette__skeleton{padding:8px 4px}.chat-history-search-palette__skeleton-row{height:36px;margin:4px 0;background:var(--tplane-chat-surface-alt);border-radius:8px;animation:tplane-chat-pulse 1.4s ease-in-out infinite}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3948
4616
  }
3949
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverflowMenuComponent, decorators: [{
4617
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatHistorySearchPaletteComponent, decorators: [{
3950
4618
  type: Component,
3951
- args: [{ selector: 'chat-overflow-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
4619
+ args: [{ selector: 'chat-history-search-palette', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3952
4620
  @if (open()) {
3953
4621
  <button
3954
4622
  type="button"
3955
- class="chat-overflow-menu__scrim"
3956
- aria-label="Close menu"
4623
+ class="chat-history-search-palette__scrim"
4624
+ aria-label="Close search"
3957
4625
  (click)="closed.emit()"
3958
4626
  ></button>
3959
- <ul
3960
- class="chat-overflow-menu"
3961
- role="menu"
3962
- tabindex="-1"
3963
- [style.top.px]="position().top"
3964
- [style.left.px]="position().left"
3965
- (keydown)="onMenuKeydown($event)"
4627
+ <div
4628
+ class="chat-history-search-palette"
4629
+ role="dialog"
4630
+ aria-modal="true"
4631
+ aria-label="Search conversations"
3966
4632
  >
3967
- @for (item of items(); track item.id) {
3968
- <li
3969
- role="menuitem"
3970
- tabindex="0"
3971
- class="chat-overflow-menu__item"
3972
- [class.chat-overflow-menu__item--destructive]="item.tone === 'destructive'"
3973
- [class.chat-overflow-menu__item--disabled]="item.disabled"
3974
- [attr.aria-disabled]="item.disabled ? 'true' : null"
3975
- [attr.tabindex]="item.disabled ? -1 : 0"
3976
- (click)="onItemClick(item)"
3977
- (keydown.enter)="onItemClick(item)"
3978
- (keydown.space)="onItemClick(item)"
4633
+ <div class="chat-history-search-palette__input-row">
4634
+ <svg class="chat-history-search-palette__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4635
+ <circle cx="11" cy="11" r="7"/>
4636
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
4637
+ </svg>
4638
+ <input
4639
+ #inputEl
4640
+ class="chat-history-search-palette__input"
4641
+ type="text"
4642
+ role="combobox"
4643
+ aria-expanded="true"
4644
+ [attr.aria-controls]="listId"
4645
+ [attr.aria-activedescendant]="activeRowId()"
4646
+ [placeholder]="placeholder()"
4647
+ [value]="query()"
4648
+ (input)="onInput($event)"
4649
+ (keydown)="onInputKeydown($event)"
4650
+ />
4651
+ <button
4652
+ type="button"
4653
+ class="chat-history-search-palette__close"
4654
+ aria-label="Close"
4655
+ (click)="closed.emit()"
3979
4656
  >
3980
- {{ item.label }}
3981
- </li>
4657
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4658
+ <line x1="18" y1="6" x2="6" y2="18"/>
4659
+ <line x1="6" y1="6" x2="18" y2="18"/>
4660
+ </svg>
4661
+ </button>
4662
+ </div>
4663
+
4664
+ @if (loading() && results().length === 0) {
4665
+ <div class="chat-history-search-palette__skeleton" aria-hidden="true">
4666
+ <div class="chat-history-search-palette__skeleton-row"></div>
4667
+ <div class="chat-history-search-palette__skeleton-row"></div>
4668
+ <div class="chat-history-search-palette__skeleton-row"></div>
4669
+ </div>
4670
+ } @else if (results().length === 0 && query().length === 0) {
4671
+ <div class="chat-history-search-palette__hint">Type to search your conversations.</div>
4672
+ } @else if (results().length === 0) {
4673
+ <div class="chat-history-search-palette__empty">No conversations match.</div>
4674
+ } @else {
4675
+ <ul class="chat-history-search-palette__list" role="listbox" [id]="listId">
4676
+ @for (row of results(); let i = $index; track row.id) {
4677
+ <li
4678
+ class="chat-history-search-palette__row"
4679
+ role="option"
4680
+ tabindex="-1"
4681
+ [id]="rowId(i)"
4682
+ [attr.aria-selected]="i === activeIndex() ? 'true' : 'false'"
4683
+ (click)="onRowClick(row.id)"
4684
+ (keydown.enter)="onRowClick(row.id)"
4685
+ (keydown.space)="onRowClick(row.id)"
4686
+ >
4687
+ <span class="chat-history-search-palette__row-title">{{ row.title }}</span>
4688
+ @if (row.subtitle) {
4689
+ <span class="chat-history-search-palette__row-subtitle">{{ row.subtitle }}</span>
4690
+ }
4691
+ </li>
4692
+ }
4693
+ </ul>
3982
4694
  }
3983
- </ul>
4695
+ </div>
3984
4696
  }
3985
- `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-overflow-menu__scrim{position:fixed;inset:0;background:transparent;z-index:59;border:0;padding:0;cursor:default}.chat-overflow-menu{position:fixed;z-index:60;min-width:160px;padding:4px;margin:0;list-style:none;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:8px;box-shadow:0 8px 24px #00000026}.chat-overflow-menu__item{display:block;padding:8px 12px;border-radius:4px;color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.chat-overflow-menu__item:hover{background:var(--tplane-chat-surface-alt)}.chat-overflow-menu__item:focus-visible{outline:2px solid var(--tplane-chat-primary);outline-offset:-2px}.chat-overflow-menu__item--destructive{color:var(--tplane-chat-error-text)}.chat-overflow-menu__item--disabled{color:var(--tplane-chat-text-muted);cursor:not-allowed;pointer-events:none}\n"] }]
3986
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], anchorPos: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorPos", required: false }] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
4697
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-history-search-palette__scrim{position:fixed;inset:0;background:#0006;z-index:var(--tplane-chat-z-modal-scrim, 1100);border:0;padding:0;cursor:pointer}.chat-history-search-palette{position:fixed;top:15vh;left:50%;transform:translate(-50%);width:min(560px,90vw);max-height:70vh;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:12px;box-shadow:0 16px 48px #00000040;z-index:var(--tplane-chat-z-modal, 1101);display:flex;flex-direction:column;overflow:hidden}.chat-history-search-palette__input-row{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--tplane-chat-separator)}.chat-history-search-palette__icon{width:18px;height:18px;color:var(--tplane-chat-text-muted);flex-shrink:0}.chat-history-search-palette__input{flex:1 1 auto;border:0;outline:none;background:transparent;color:var(--tplane-chat-text);font:inherit;font-size:1rem}.chat-history-search-palette__input::placeholder{color:var(--tplane-chat-text-muted)}.chat-history-search-palette__close{background:transparent;border:0;padding:4px;color:var(--tplane-chat-text-muted);cursor:pointer;border-radius:4px}.chat-history-search-palette__close:hover{color:var(--tplane-chat-text)}.chat-history-search-palette__list{flex:1 1 auto;overflow-y:auto;padding:4px;margin:0;list-style:none}.chat-history-search-palette__row{display:flex;flex-direction:column;padding:10px 12px;border-radius:8px;cursor:pointer}.chat-history-search-palette__row[aria-selected=true]{background:var(--tplane-chat-surface-alt)}.chat-history-search-palette__row-title{color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size)}.chat-history-search-palette__row-subtitle{color:var(--tplane-chat-text-muted);font-size:var(--tplane-chat-font-size-sm);margin-top:2px}.chat-history-search-palette__empty,.chat-history-search-palette__hint{padding:24px 16px;color:var(--tplane-chat-text-muted);text-align:center;font-size:var(--tplane-chat-font-size-sm)}.chat-history-search-palette__skeleton{padding:8px 4px}.chat-history-search-palette__skeleton-row{height:36px;margin:4px 0;background:var(--tplane-chat-surface-alt);border-radius:8px;animation:tplane-chat-pulse 1.4s ease-in-out infinite}\n"] }]
4698
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }], results: [{ type: i0.Input, args: [{ isSignal: true, alias: "results", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], threadSelected: [{ type: i0.Output, args: ["threadSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
4699
+
4700
+ // libs/chat/src/lib/styles/chat-overflow-menu.styles.ts
4701
+ // SPDX-License-Identifier: MIT
4702
+ const CHAT_OVERFLOW_MENU_STYLES = `
4703
+ :host { display: contents; }
4704
+ .chat-overflow-menu__scrim {
4705
+ position: fixed;
4706
+ inset: 0;
4707
+ background: transparent;
4708
+ z-index: 59;
4709
+ border: 0;
4710
+ padding: 0;
4711
+ cursor: default;
4712
+ }
4713
+ .chat-overflow-menu {
4714
+ position: fixed;
4715
+ z-index: 60;
4716
+ min-width: 160px;
4717
+ padding: 4px;
4718
+ margin: 0;
4719
+ list-style: none;
4720
+ background: var(--tplane-chat-bg);
4721
+ border: 1px solid var(--tplane-chat-separator);
4722
+ border-radius: 8px;
4723
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
4724
+ }
4725
+ .chat-overflow-menu__item {
4726
+ display: block;
4727
+ padding: 8px 12px;
4728
+ border-radius: 4px;
4729
+ color: var(--tplane-chat-text);
4730
+ font-size: var(--tplane-chat-font-size-sm);
4731
+ cursor: pointer;
4732
+ user-select: none;
4733
+ }
4734
+ .chat-overflow-menu__item:hover {
4735
+ background: var(--tplane-chat-surface-alt);
4736
+ }
4737
+ .chat-overflow-menu__item:focus-visible {
4738
+ outline: 2px solid var(--tplane-chat-primary);
4739
+ outline-offset: -2px;
4740
+ }
4741
+ .chat-overflow-menu__item--destructive {
4742
+ color: var(--tplane-chat-error-text);
4743
+ }
4744
+ .chat-overflow-menu__item--disabled {
4745
+ color: var(--tplane-chat-text-muted);
4746
+ cursor: not-allowed;
4747
+ pointer-events: none;
4748
+ }
4749
+ `;
4750
+
4751
+ // libs/chat/src/lib/primitives/chat-overflow-menu/chat-overflow-menu.component.ts
4752
+ // SPDX-License-Identifier: MIT
4753
+ class ChatOverflowMenuComponent {
4754
+ open = input(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4755
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
4756
+ /** Element the menu anchors against (positions just below its bottom-right corner). */
4757
+ anchor = input(null, ...(ngDevMode ? [{ debugName: "anchor" }] : []));
4758
+ /** Alternative anchor: explicit viewport coordinates (e.g. cursor position
4759
+ * from a right-click). Takes precedence over `anchor` when set. */
4760
+ anchorPos = input(null, ...(ngDevMode ? [{ debugName: "anchorPos" }] : []));
4761
+ itemSelected = output();
4762
+ closed = output();
4763
+ position = computed(() => {
4764
+ if (!this.open())
4765
+ return { top: 0, left: 0 };
4766
+ const pos = this.anchorPos();
4767
+ if (pos) {
4768
+ return { top: pos.y + 4, left: Math.max(pos.x, 8) };
4769
+ }
4770
+ const el = this.anchor();
4771
+ if (!el) {
4772
+ const vw = typeof window === 'undefined' ? 0 : window.innerWidth;
4773
+ const vh = typeof window === 'undefined' ? 0 : window.innerHeight;
4774
+ return { top: Math.max(vh / 3, 0), left: Math.max(vw / 2 - 80, 0) };
4775
+ }
4776
+ const rect = el.getBoundingClientRect();
4777
+ return { top: rect.bottom + 4, left: Math.max(rect.right - 160, 8) };
4778
+ }, ...(ngDevMode ? [{ debugName: "position" }] : []));
4779
+ constructor() {
4780
+ effect(() => {
4781
+ if (!this.open())
4782
+ return;
4783
+ queueMicrotask(() => {
4784
+ const root = document.querySelector('.chat-overflow-menu');
4785
+ const first = root?.querySelector('.chat-overflow-menu__item:not(.chat-overflow-menu__item--disabled)');
4786
+ first?.focus();
4787
+ });
4788
+ });
4789
+ }
4790
+ onItemClick(item) {
4791
+ if (item.disabled)
4792
+ return;
4793
+ this.itemSelected.emit(item.id);
4794
+ this.closed.emit();
4795
+ }
4796
+ onMenuKeydown(e) {
4797
+ if (e.key === 'Escape') {
4798
+ e.preventDefault();
4799
+ this.closed.emit();
4800
+ return;
4801
+ }
4802
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
4803
+ e.preventDefault();
4804
+ const root = e.currentTarget;
4805
+ const items = Array.from(root.querySelectorAll('.chat-overflow-menu__item:not(.chat-overflow-menu__item--disabled)'));
4806
+ if (items.length === 0)
4807
+ return;
4808
+ const current = document.activeElement;
4809
+ const idx = current ? items.indexOf(current) : -1;
4810
+ const next = e.key === 'ArrowDown'
4811
+ ? Math.min((idx < 0 ? 0 : idx + 1), items.length - 1)
4812
+ : Math.max(idx - 1, 0);
4813
+ items[next]?.focus();
4814
+ }
4815
+ }
4816
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverflowMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4817
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatOverflowMenuComponent, isStandalone: true, selector: "chat-overflow-menu", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, anchorPos: { classPropertyName: "anchorPos", publicName: "anchorPos", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelected: "itemSelected", closed: "closed" }, ngImport: i0, template: `
4818
+ @if (open()) {
4819
+ <button
4820
+ type="button"
4821
+ class="chat-overflow-menu__scrim"
4822
+ aria-label="Close menu"
4823
+ (click)="closed.emit()"
4824
+ ></button>
4825
+ <ul
4826
+ class="chat-overflow-menu"
4827
+ role="menu"
4828
+ tabindex="-1"
4829
+ [style.top.px]="position().top"
4830
+ [style.left.px]="position().left"
4831
+ (keydown)="onMenuKeydown($event)"
4832
+ >
4833
+ @for (item of items(); track item.id) {
4834
+ <li
4835
+ role="menuitem"
4836
+ tabindex="0"
4837
+ class="chat-overflow-menu__item"
4838
+ [class.chat-overflow-menu__item--destructive]="item.tone === 'destructive'"
4839
+ [class.chat-overflow-menu__item--disabled]="item.disabled"
4840
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
4841
+ [attr.tabindex]="item.disabled ? -1 : 0"
4842
+ (click)="onItemClick(item)"
4843
+ (keydown.enter)="onItemClick(item)"
4844
+ (keydown.space)="onItemClick(item)"
4845
+ >
4846
+ {{ item.label }}
4847
+ </li>
4848
+ }
4849
+ </ul>
4850
+ }
4851
+ `, isInline: true, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-overflow-menu__scrim{position:fixed;inset:0;background:transparent;z-index:59;border:0;padding:0;cursor:default}.chat-overflow-menu{position:fixed;z-index:60;min-width:160px;padding:4px;margin:0;list-style:none;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:8px;box-shadow:0 8px 24px #00000026}.chat-overflow-menu__item{display:block;padding:8px 12px;border-radius:4px;color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.chat-overflow-menu__item:hover{background:var(--tplane-chat-surface-alt)}.chat-overflow-menu__item:focus-visible{outline:2px solid var(--tplane-chat-primary);outline-offset:-2px}.chat-overflow-menu__item--destructive{color:var(--tplane-chat-error-text)}.chat-overflow-menu__item--disabled{color:var(--tplane-chat-text-muted);cursor:not-allowed;pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4852
+ }
4853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverflowMenuComponent, decorators: [{
4854
+ type: Component,
4855
+ args: [{ selector: 'chat-overflow-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
4856
+ @if (open()) {
4857
+ <button
4858
+ type="button"
4859
+ class="chat-overflow-menu__scrim"
4860
+ aria-label="Close menu"
4861
+ (click)="closed.emit()"
4862
+ ></button>
4863
+ <ul
4864
+ class="chat-overflow-menu"
4865
+ role="menu"
4866
+ tabindex="-1"
4867
+ [style.top.px]="position().top"
4868
+ [style.left.px]="position().left"
4869
+ (keydown)="onMenuKeydown($event)"
4870
+ >
4871
+ @for (item of items(); track item.id) {
4872
+ <li
4873
+ role="menuitem"
4874
+ tabindex="0"
4875
+ class="chat-overflow-menu__item"
4876
+ [class.chat-overflow-menu__item--destructive]="item.tone === 'destructive'"
4877
+ [class.chat-overflow-menu__item--disabled]="item.disabled"
4878
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
4879
+ [attr.tabindex]="item.disabled ? -1 : 0"
4880
+ (click)="onItemClick(item)"
4881
+ (keydown.enter)="onItemClick(item)"
4882
+ (keydown.space)="onItemClick(item)"
4883
+ >
4884
+ {{ item.label }}
4885
+ </li>
4886
+ }
4887
+ </ul>
4888
+ }
4889
+ `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:contents}.chat-overflow-menu__scrim{position:fixed;inset:0;background:transparent;z-index:59;border:0;padding:0;cursor:default}.chat-overflow-menu{position:fixed;z-index:60;min-width:160px;padding:4px;margin:0;list-style:none;background:var(--tplane-chat-bg);border:1px solid var(--tplane-chat-separator);border-radius:8px;box-shadow:0 8px 24px #00000026}.chat-overflow-menu__item{display:block;padding:8px 12px;border-radius:4px;color:var(--tplane-chat-text);font-size:var(--tplane-chat-font-size-sm);cursor:pointer;-webkit-user-select:none;user-select:none}.chat-overflow-menu__item:hover{background:var(--tplane-chat-surface-alt)}.chat-overflow-menu__item:focus-visible{outline:2px solid var(--tplane-chat-primary);outline-offset:-2px}.chat-overflow-menu__item--destructive{color:var(--tplane-chat-error-text)}.chat-overflow-menu__item--disabled{color:var(--tplane-chat-text-muted);cursor:not-allowed;pointer-events:none}\n"] }]
4890
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], anchorPos: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorPos", required: false }] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
3987
4891
 
3988
4892
  // libs/chat/src/lib/styles/chat-confirm-dialog.styles.ts
3989
4893
  // SPDX-License-Identifier: MIT
@@ -6755,283 +7659,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
6755
7659
  `, styles: [":host{font-family:var(--tplane-chat-font-family);color:var(--tplane-chat-text)}\n", ":host{display:inline-block}.chat-welcome-suggestion{display:inline-flex;align-items:center;gap:.5rem;padding:10px 16px;background:var(--tplane-chat-surface);border:1px solid var(--tplane-chat-separator);border-radius:9999px;color:var(--tplane-chat-text);font-family:inherit;font-size:var(--tplane-chat-font-size-sm);text-align:center;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .12s ease}.chat-welcome-suggestion:hover{background:var(--tplane-chat-surface-alt);border-color:var(--tplane-chat-text-muted)}.chat-welcome-suggestion:active{transform:scale(.98)}.chat-welcome-suggestion:focus-visible{outline:2px solid var(--tplane-chat-text-muted);outline-offset:2px}.chat-welcome-suggestion__label{white-space:nowrap}.chat-welcome-suggestion__chevron{display:none}\n"] }]
6756
7660
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
6757
7661
 
6758
- // libs/chat/src/lib/primitives/overlay/overlay-container.ts
6759
- // SPDX-License-Identifier: MIT
6760
- const CONTAINER_CLASS = 'chat-overlay-container';
6761
- const STYLE_ID = 'chat-overlay-structure';
6762
- // Structural CSS, injected once into <head> (same pattern as ROOT_TOKEN_STYLES
6763
- // in chat-tokens.ts) so consumers need not import any stylesheet.
6764
- const STRUCTURE_CSS = `
6765
- .${CONTAINER_CLASS} {
6766
- position: fixed;
6767
- inset: 0;
6768
- z-index: 1000;
6769
- pointer-events: none;
6770
- }
6771
- .chat-overlay-pane {
6772
- position: absolute;
6773
- pointer-events: auto;
6774
- }
6775
- `;
6776
- /** Returns the single shared overlay container appended to <body>, creating it
6777
- * (and injecting structural CSS) on first call. */
6778
- function getOverlayContainer(doc) {
6779
- const existing = doc.querySelector('.' + CONTAINER_CLASS);
6780
- if (existing)
6781
- return existing;
6782
- if (!doc.getElementById(STYLE_ID)) {
6783
- const style = doc.createElement('style');
6784
- style.id = STYLE_ID;
6785
- style.textContent = STRUCTURE_CSS;
6786
- doc.head.appendChild(style);
6787
- }
6788
- const container = doc.createElement('div');
6789
- container.className = CONTAINER_CLASS;
6790
- doc.body.appendChild(container);
6791
- return container;
6792
- }
6793
-
6794
- // libs/chat/src/lib/primitives/overlay/connected-position.ts
6795
- // SPDX-License-Identifier: MIT
6796
- //
6797
- // Minimal port of CDK's FlexibleConnectedPositionStrategy fit logic
6798
- // (~/repos/components/src/cdk/overlay/position/flexible-connected-position-strategy.ts):
6799
- // _getOriginPoint + _getOverlayPoint + _getOverlayFit + _pushOverlayOnScreen.
6800
- // Omits flexible-dimensions, grow-after-open, RTL, and virtual-keyboard handling.
6801
- function originPoint(origin, pos) {
6802
- const x = pos.originX === 'center' ? origin.left + origin.width / 2 : pos.originX === 'start' ? origin.left : origin.right;
6803
- const y = pos.originY === 'center' ? origin.top + origin.height / 2 : pos.originY === 'top' ? origin.top : origin.bottom;
6804
- return { x, y };
6805
- }
6806
- function overlayPoint(origin, size, pos) {
6807
- let x = origin.x;
6808
- if (pos.overlayX === 'center')
6809
- x -= size.width / 2;
6810
- else if (pos.overlayX === 'end')
6811
- x -= size.width;
6812
- let y = origin.y;
6813
- if (pos.overlayY === 'center')
6814
- y -= size.height / 2;
6815
- else if (pos.overlayY === 'bottom')
6816
- y -= size.height;
6817
- return { x: x + (pos.offsetX ?? 0), y: y + (pos.offsetY ?? 0) };
6818
- }
6819
- function fitArea(point, size, viewport) {
6820
- const left = point.x;
6821
- const right = point.x + size.width;
6822
- const top = point.y;
6823
- const bottom = point.y + size.height;
6824
- const visibleW = Math.max(0, Math.min(right, viewport.right) - Math.max(left, viewport.left));
6825
- const visibleH = Math.max(0, Math.min(bottom, viewport.bottom) - Math.max(top, viewport.top));
6826
- const fits = left >= viewport.left && right <= viewport.right && top >= viewport.top && bottom <= viewport.bottom;
6827
- return { area: visibleW * visibleH, fits };
6828
- }
6829
- function pushOnScreen(point, size, viewport) {
6830
- const maxLeft = viewport.right - size.width;
6831
- const maxTop = viewport.bottom - size.height;
6832
- return {
6833
- x: Math.max(viewport.left, Math.min(point.x, maxLeft)),
6834
- y: Math.max(viewport.top, Math.min(point.y, maxTop)),
6835
- };
6836
- }
6837
- /** Sensible fallback when no positions are supplied: below the origin, start-aligned. */
6838
- const DEFAULT_POSITION = { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' };
6839
- function computeConnectedPosition(args) {
6840
- const { originRect, overlaySize, viewport, positions } = args;
6841
- // An empty positions array (the directive's input default) must still anchor to
6842
- // the trigger rather than crash on best! / render at 0,0.
6843
- const list = positions.length ? positions : [DEFAULT_POSITION];
6844
- let best = null;
6845
- for (const pos of list) {
6846
- const point = overlayPoint(originPoint(originRect, pos), overlaySize, pos);
6847
- const { area, fits } = fitArea(point, overlaySize, viewport);
6848
- if (fits) {
6849
- return { top: point.y, left: point.x, position: pos };
6850
- }
6851
- if (!best || area > best.area)
6852
- best = { point, pos, area };
6853
- }
6854
- const pushed = pushOnScreen(best.point, overlaySize, viewport);
6855
- return { top: pushed.y, left: pushed.x, position: best.pos };
6856
- }
6857
- function narrowViewport(win, margin) {
6858
- return {
6859
- left: margin,
6860
- top: margin,
6861
- right: win.innerWidth - margin,
6862
- bottom: win.innerHeight - margin,
6863
- width: win.innerWidth - 2 * margin,
6864
- height: win.innerHeight - 2 * margin,
6865
- };
6866
- }
6867
-
6868
- // libs/chat/src/lib/primitives/overlay/connected-overlay.directive.ts
6869
- // SPDX-License-Identifier: MIT
6870
- /* eslint-disable @angular-eslint/no-input-rename, @angular-eslint/no-output-rename --
6871
- * The `chatOverlay*` binding aliases ARE the intended public API: they namespace
6872
- * the directive's inputs/outputs under the `chatOverlay` prefix (mirroring Angular
6873
- * CDK's `cdkConnectedOverlay*` convention) rather than exposing bare names like
6874
- * `open`/`positions` on an `<ng-template>`. The internal property names stay
6875
- * concise, so aliasing here is deliberate, not a rename to avoid. */
6876
- /** Marks the anchor element a connected overlay positions against. */
6877
- class ChatOverlayOriginDirective {
6878
- elementRef = inject(ElementRef);
6879
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverlayOriginDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6880
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.6", type: ChatOverlayOriginDirective, isStandalone: true, selector: "[chatOverlayOrigin]", exportAs: ["chatOverlayOrigin"], ngImport: i0 });
6881
- }
6882
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatOverlayOriginDirective, decorators: [{
6883
- type: Directive,
6884
- args: [{
6885
- selector: '[chatOverlayOrigin]',
6886
- standalone: true,
6887
- exportAs: 'chatOverlayOrigin',
6888
- }]
6889
- }] });
6890
- const VIEWPORT_MARGIN = 8;
6891
- /**
6892
- * Applied to an `<ng-template>`. When `chatOverlayOpen` is true, the template
6893
- * content is portaled into the shared body-level overlay container and
6894
- * positioned connected to `chatOverlayOrigin`, repositioning live on
6895
- * scroll/resize. Closes on outside mousedown (via the `chatOverlayOutsideClick`
6896
- * output) and Tab; returns focus to the origin when focus was inside the pane.
6897
- *
6898
- * NOTE: the directive does not own the open state — it only emits. To fully
6899
- * close the overlay, consumers MUST handle BOTH `(chatOverlayOutsideClick)` and
6900
- * `(chatOverlayDetach)`. Tab-close (and Escape, when the consumer routes it)
6901
- * surface through `chatOverlayDetach`, so wiring only `chatOverlayOutsideClick`
6902
- * leaves the overlay stuck open on Tab.
6903
- */
6904
- class ChatConnectedOverlayDirective {
6905
- origin = input.required({ ...(ngDevMode ? { debugName: "origin" } : {}), alias: 'chatOverlayOrigin' });
6906
- open = input(false, { ...(ngDevMode ? { debugName: "open" } : {}), alias: 'chatOverlayOpen' });
6907
- positions = input([], { ...(ngDevMode ? { debugName: "positions" } : {}), alias: 'chatOverlayPositions' });
6908
- panelClass = input('', { ...(ngDevMode ? { debugName: "panelClass" } : {}), alias: 'chatOverlayPanelClass' });
6909
- /** Emits the pane element once attached (consumers focus content from it). */
6910
- attached = output({ alias: 'chatOverlayAttached' });
6911
- outsideClick = output({ alias: 'chatOverlayOutsideClick' });
6912
- detached = output({ alias: 'chatOverlayDetach' });
6913
- templateRef = inject((TemplateRef));
6914
- viewContainerRef = inject(ViewContainerRef);
6915
- document = inject(DOCUMENT);
6916
- pane = null;
6917
- viewRef = null;
6918
- resizeObs = null;
6919
- rafId = 0;
6920
- previouslyFocused = null;
6921
- onScrollOrResize = () => this.scheduleReposition();
6922
- onDocMouseDown = (e) => {
6923
- if (!this.pane)
6924
- return;
6925
- const path = e.composedPath();
6926
- if (path.includes(this.pane) || path.includes(this.origin().elementRef.nativeElement))
6927
- return;
6928
- this.outsideClick.emit(e);
6929
- };
6930
- onKeydown = (e) => {
6931
- if (e.key !== 'Tab' || !this.pane)
6932
- return;
6933
- const active = this.document.activeElement;
6934
- if (this.pane.contains(active) || active === this.origin().elementRef.nativeElement) {
6935
- this.detached.emit();
6936
- }
6937
- };
6938
- constructor() {
6939
- effect(() => {
6940
- if (this.open())
6941
- this.attach();
6942
- else
6943
- this.dispose();
6944
- });
6945
- inject(DestroyRef).onDestroy(() => this.dispose());
6946
- }
6947
- attach() {
6948
- if (this.pane)
6949
- return;
6950
- const win = this.document.defaultView;
6951
- if (!win)
6952
- return; // SSR / detached document
6953
- this.previouslyFocused = this.document.activeElement;
6954
- const pane = this.document.createElement('div');
6955
- pane.className = 'chat-overlay-pane';
6956
- for (const c of this.normalizePanelClass())
6957
- pane.classList.add(c);
6958
- getOverlayContainer(this.document).appendChild(pane);
6959
- this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);
6960
- this.viewRef.detectChanges();
6961
- for (const node of this.viewRef.rootNodes)
6962
- pane.appendChild(node);
6963
- this.pane = pane;
6964
- this.reposition();
6965
- win.addEventListener('scroll', this.onScrollOrResize, { capture: true, passive: true });
6966
- win.addEventListener('resize', this.onScrollOrResize, { passive: true });
6967
- this.document.addEventListener('mousedown', this.onDocMouseDown, true);
6968
- this.document.addEventListener('keydown', this.onKeydown, true);
6969
- if (typeof win.ResizeObserver === 'function') {
6970
- this.resizeObs = new win.ResizeObserver(() => this.scheduleReposition());
6971
- this.resizeObs.observe(this.origin().elementRef.nativeElement);
6972
- this.resizeObs.observe(pane);
6973
- }
6974
- this.attached.emit(pane);
6975
- }
6976
- scheduleReposition() {
6977
- const win = this.document.defaultView;
6978
- if (!win || !this.pane)
6979
- return;
6980
- if (this.rafId)
6981
- win.cancelAnimationFrame(this.rafId);
6982
- this.rafId = win.requestAnimationFrame(() => this.reposition());
6983
- }
6984
- reposition() {
6985
- const win = this.document.defaultView;
6986
- if (!win || !this.pane)
6987
- return;
6988
- const r = this.pane.getBoundingClientRect();
6989
- const result = computeConnectedPosition({
6990
- originRect: this.origin().elementRef.nativeElement.getBoundingClientRect(),
6991
- overlaySize: { width: r.width, height: r.height },
6992
- viewport: narrowViewport(win, VIEWPORT_MARGIN),
6993
- positions: this.positions(),
6994
- });
6995
- this.pane.style.top = `${Math.round(result.top)}px`;
6996
- this.pane.style.left = `${Math.round(result.left)}px`;
6997
- }
6998
- dispose() {
6999
- const win = this.document.defaultView;
7000
- if (this.rafId && win)
7001
- win.cancelAnimationFrame(this.rafId);
7002
- this.rafId = 0;
7003
- if (win) {
7004
- win.removeEventListener('scroll', this.onScrollOrResize, { capture: true });
7005
- win.removeEventListener('resize', this.onScrollOrResize);
7006
- }
7007
- this.document.removeEventListener('mousedown', this.onDocMouseDown, true);
7008
- this.document.removeEventListener('keydown', this.onKeydown, true);
7009
- this.resizeObs?.disconnect();
7010
- this.resizeObs = null;
7011
- const focusWasInPane = !!this.pane && this.pane.contains(this.document.activeElement);
7012
- this.viewRef?.destroy();
7013
- this.viewRef = null;
7014
- this.pane?.remove();
7015
- this.pane = null;
7016
- if (focusWasInPane && this.previouslyFocused)
7017
- this.previouslyFocused.focus();
7018
- this.previouslyFocused = null;
7019
- }
7020
- normalizePanelClass() {
7021
- const pc = this.panelClass();
7022
- return Array.isArray(pc) ? pc : pc ? [pc] : [];
7023
- }
7024
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatConnectedOverlayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7025
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.6", type: ChatConnectedOverlayDirective, isStandalone: true, selector: "[chatConnectedOverlay]", inputs: { origin: { classPropertyName: "origin", publicName: "chatOverlayOrigin", isSignal: true, isRequired: true, transformFunction: null }, open: { classPropertyName: "open", publicName: "chatOverlayOpen", isSignal: true, isRequired: false, transformFunction: null }, positions: { classPropertyName: "positions", publicName: "chatOverlayPositions", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "chatOverlayPanelClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { attached: "chatOverlayAttached", outsideClick: "chatOverlayOutsideClick", detached: "chatOverlayDetach" }, ngImport: i0 });
7026
- }
7027
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatConnectedOverlayDirective, decorators: [{
7028
- type: Directive,
7029
- args: [{
7030
- selector: '[chatConnectedOverlay]',
7031
- standalone: true,
7032
- }]
7033
- }], ctorParameters: () => [], propDecorators: { origin: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayOrigin", required: true }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayOpen", required: false }] }], positions: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayPositions", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatOverlayPanelClass", required: false }] }], attached: [{ type: i0.Output, args: ["chatOverlayAttached"] }], outsideClick: [{ type: i0.Output, args: ["chatOverlayOutsideClick"] }], detached: [{ type: i0.Output, args: ["chatOverlayDetach"] }] } });
7034
-
7035
7662
  // libs/chat/src/lib/styles/chat-select.styles.ts
7036
7663
  // SPDX-License-Identifier: MIT
7037
7664
  const CHAT_SELECT_STYLES = `
@@ -9387,8 +10014,8 @@ class ChatComponent {
9387
10014
  *
9388
10015
  * Matches the same `streaming + current` condition the bubble uses
9389
10016
  * to enable `.chat-message__caret`:
9390
- * `agent().isLoading() && i === agent().messages().length - 1`
9391
- * `i === agent().messages().length - 1`
10017
+ * `this.agent().isLoading() && i === this.agent().messages().length - 1`
10018
+ * `i === this.agent().messages().length - 1`
9392
10019
  *
9393
10020
  * Restricted to assistant role because the caret only renders on
9394
10021
  * assistant bubbles (`:host([data-role="assistant"][data-current=...
@@ -13158,5 +13785,5 @@ function mockAgent(opts = {}) {
13158
13785
  * Generated bundle index. Do not edit.
13159
13786
  */
13160
13787
 
13161
- export { A2uiAudioPlayerComponent, A2uiButtonComponent, A2uiCardComponent, A2uiCheckBoxComponent, A2uiColumnComponent, A2uiDateTimeInputComponent, A2uiDividerComponent, A2uiIconComponent, A2uiImageComponent, A2uiListComponent, A2uiModalComponent, A2uiMultipleChoiceComponent, A2uiRowComponent, A2uiSliderComponent, A2uiSurfaceComponent, A2uiTabsComponent, A2uiTextComponent, A2uiTextFieldComponent, A2uiVideoComponent, AGENT_ERROR_MESSAGES, AgentError, CHAT_CONFIG, CHAT_LIFECYCLE, ChatApprovalCardComponent, ChatCitationCardTemplateDirective, ChatCitationsCardComponent, ChatCitationsComponent, ChatComponent, ChatConfirmDialogComponent, ChatConnectedOverlayDirective, ChatErrorComponent, ChatGenerativeUiComponent, ChatGenuiSkeletonComponent, ChatHistorySearchPaletteComponent, ChatInputComponent, ChatInterruptComponent, ChatInterruptPanelComponent, ChatLauncherButtonComponent, ChatMessageActionsComponent, ChatMessageComponent, ChatMessageListComponent, ChatOverflowMenuComponent, ChatOverlayOriginDirective, ChatPopupComponent, ChatProjectListComponent, ChatReasoningComponent, ChatScrollBubbleComponent, ChatSelectComponent, ChatSidebarComponent, ChatSidenavComponent, ChatSidenavScrimComponent, ChatStreamingMdComponent, ChatSubagentCardComponent, ChatSubagentsComponent, ChatSuggestionsComponent, ChatThreadListComponent, ChatTimelineComponent, ChatTimelineSliderComponent, ChatToolCallCardComponent, ChatToolCallTemplateDirective, ChatToolCallsComponent, ChatToolViewsComponent, ChatTraceComponent, ChatTypingIndicatorComponent, ChatWelcomeComponent, ChatWelcomeSuggestionComponent, ChatWindowComponent, CitationsResolverService, IS_HEADER_ROW, MARKDOWN_VIEW_REGISTRY, MarkdownAutolinkComponent, MarkdownBlockquoteComponent, MarkdownChildrenComponent, MarkdownCitationReferenceComponent, MarkdownCodeBlockComponent, MarkdownDocumentComponent, MarkdownEmphasisComponent, MarkdownHardBreakComponent, MarkdownHeadingComponent, MarkdownHtmlComponent, MarkdownImageComponent, MarkdownInlineCodeComponent, MarkdownLinkComponent, MarkdownListComponent, MarkdownListItemComponent, MarkdownMathComponent, MarkdownParagraphComponent, MarkdownSoftBreakComponent, MarkdownStrikethroughComponent, MarkdownStrongComponent, MarkdownTableCellComponent, MarkdownTableComponent, MarkdownTableRowComponent, MarkdownTextComponent, MarkdownThematicBreakComponent, MessageTemplateDirective, a2uiBasicCatalog, action, ask, buildA2uiActionMessage, cacheplaneMarkdownViews, createA2uiSurfaceStore, createAgentRef, createContentClassifier, createParseTreeStore, createPartialArgsBridge, deriveJsonSchema, emitBinding, executeFunctionTool, extractErrorMessage, formatDuration, getInterrupt, getMessageType, injectThreadRouting, isAbortError, isAssistantMessage, isSystemMessage, isToolMessage, isTyping, isUserMessage, messageContent, mockAgent, normalizeEnvelopeArgs, normalizeViewEntry, provideChat, renderMarkdown, startClientToolExecutor, statusColor, submitMessage, toAgentError, toClientToolSpecs, tools, validateArgs, view };
13788
+ export { A2uiAudioPlayerComponent, A2uiButtonComponent, A2uiCardComponent, A2uiCheckBoxComponent, A2uiColumnComponent, A2uiDateTimeInputComponent, A2uiDividerComponent, A2uiIconComponent, A2uiImageComponent, A2uiListComponent, A2uiModalComponent, A2uiMultipleChoiceComponent, A2uiRowComponent, A2uiSliderComponent, A2uiSurfaceComponent, A2uiTabsComponent, A2uiTextComponent, A2uiTextFieldComponent, A2uiVideoComponent, AGENT_ERROR_MESSAGES, AgentError, CHAT_CONFIG, CHAT_LIFECYCLE, ChatApprovalCardComponent, ChatCitationCardTemplateDirective, ChatCitationPreviewComponent, ChatCitationsCardComponent, ChatCitationsComponent, ChatComponent, ChatConfirmDialogComponent, ChatConnectedOverlayDirective, ChatErrorComponent, ChatGenerativeUiComponent, ChatGenuiSkeletonComponent, ChatHistorySearchPaletteComponent, ChatInputComponent, ChatInterruptComponent, ChatInterruptPanelComponent, ChatLauncherButtonComponent, ChatMessageActionsComponent, ChatMessageComponent, ChatMessageListComponent, ChatOverflowMenuComponent, ChatOverlayOriginDirective, ChatPopupComponent, ChatProjectListComponent, ChatReasoningComponent, ChatScrollBubbleComponent, ChatSelectComponent, ChatSidebarComponent, ChatSidenavComponent, ChatSidenavScrimComponent, ChatStreamingMdComponent, ChatSubagentCardComponent, ChatSubagentsComponent, ChatSuggestionsComponent, ChatThreadListComponent, ChatTimelineComponent, ChatTimelineSliderComponent, ChatToolCallCardComponent, ChatToolCallTemplateDirective, ChatToolCallsComponent, ChatToolViewsComponent, ChatTraceComponent, ChatTypingIndicatorComponent, ChatWelcomeComponent, ChatWelcomeSuggestionComponent, ChatWindowComponent, CitationsResolverService, IS_HEADER_ROW, MARKDOWN_VIEW_REGISTRY, MarkdownAutolinkComponent, MarkdownBlockquoteComponent, MarkdownChildrenComponent, MarkdownCitationReferenceComponent, MarkdownCodeBlockComponent, MarkdownDocumentComponent, MarkdownEmphasisComponent, MarkdownHardBreakComponent, MarkdownHeadingComponent, MarkdownHtmlComponent, MarkdownImageComponent, MarkdownInlineCodeComponent, MarkdownLinkComponent, MarkdownListComponent, MarkdownListItemComponent, MarkdownMathComponent, MarkdownParagraphComponent, MarkdownSoftBreakComponent, MarkdownStrikethroughComponent, MarkdownStrongComponent, MarkdownTableCellComponent, MarkdownTableComponent, MarkdownTableRowComponent, MarkdownTextComponent, MarkdownThematicBreakComponent, MessageTemplateDirective, a2uiBasicCatalog, action, ask, buildA2uiActionMessage, cacheplaneMarkdownViews, citationTypeLabel, createA2uiSurfaceStore, createAgentRef, createContentClassifier, createParseTreeStore, createPartialArgsBridge, deriveDomain, deriveJsonSchema, deriveMonogram, deriveSourceType, emitBinding, executeFunctionTool, extractErrorMessage, formatDuration, formatPublished, getInterrupt, getMessageType, injectThreadRouting, isAbortError, isAssistantMessage, isSystemMessage, isToolMessage, isTyping, isUserMessage, messageContent, mockAgent, monogramColor, monogramHue, normalizeEnvelopeArgs, normalizeViewEntry, provideChat, renderMarkdown, startClientToolExecutor, statusColor, submitMessage, toAgentError, toClientToolSpecs, tools, validateArgs, view };
13162
13789
  //# sourceMappingURL=threadplane-chat.mjs.map