@m6d/cortex-angular 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ No root-level providers are needed. Each widget is self-contained, so one page c
42
42
  | `viewMode` | `'helper' \| 'full'` | Compact panel or full layout with a sidebar (default: `'helper'`) |
43
43
  | `theme` | `'light' \| 'dark' \| 'system'` | Color scheme (default: `'light'`) |
44
44
  | `agentName` | `string \| { en, ar }` | Header title while no thread is selected (optional) |
45
- | `showDebugButton` | `boolean` | Show/hide the debug toggle button (default: `false`) |
45
+ | `thread` | `string` | A thread id to open on mount (optional) |
46
46
  | `mediaRenderers` | `Record<string, MediaRenderer>` | Replace or add the media fences the assistant can draw (optional) |
47
47
  | `trustSameOriginEmbeds` | `boolean` | Allow embedded tool pages served from this page's own origin (default: `false`) |
48
48
  | `hooks.onToolCall` | `(toolCall, context) => unknown` | Answers a client tool; return `undefined` for tools you do not own (optional) |
@@ -51,7 +51,7 @@ No root-level providers are needed. Each widget is self-contained, so one page c
51
51
  | `toolComponents` | `Record<string, Type<CortexToolComponentInputs>>` | Custom Angular components for tool rendering (optional) |
52
52
  | `injector` | `Injector` | Injector for `toolComponents` (optional) |
53
53
 
54
- `transport.getHeaders` returns the headers every call carries, usually a bearer `Authorization`. Called straight from another origin, the engine accepts only `Authorization` and the headers the widget sets itself; route anything else through a same-origin proxy. The WebSocket takes its `token` query param from a bearer `Authorization` header when there is one.
54
+ `transport.getHeaders` returns the headers every call carries, usually a bearer `Authorization`. Called straight from another origin, the engine accepts only `Authorization` and the headers the widget sets itself; route anything else through a same-origin proxy. The WebSocket takes its `token` query param from a bearer `Authorization` header when there is one. The debug toggle shows only when Cortex offers this caller the debug view: always to a console operator, and to everyone only where the engine runs with `ENGINE_DEBUG=true`. Anyone else gets only what the widget's normal view draws, and so does a `toolComponents` entry for one of the agent's server tools: the model's line for the user as the call's input, and only whether it failed (or a dataset read's row count) as its output.
55
55
 
56
56
  ## Custom parts
57
57
 
@@ -119,19 +119,19 @@ for typing your own rows and chips.
119
119
 
120
120
  One header serves both screens; `screen` says which is showing.
121
121
 
122
- | Field | Type | Description |
123
- | -------------- | --------------------- | ------------------------------------------------------------------ |
124
- | `screen` | `'chat' \| 'threads'` | Which screen the header is on |
125
- | `viewMode` | `'helper' \| 'full'` | The configured view mode |
126
- | `title` | `string \| null` | The open thread's title; `null` on a new chat |
127
- | `agentName` | `string \| undefined` | `agentName` in the widget's locale |
128
- | `threadCount` | `number \| undefined` | How many conversations the history lists; undefined while it loads |
129
- | `isWorking` | `boolean` | The agent is answering |
130
- | `debug` | `{ shown, on }` | Whether `showDebugButton` is set, and whether debug mode is on |
131
- | `newChat` | `() => void` | Starts a new chat |
132
- | `openHistory` | `() => void` | Shows the history: its screen, or the sidebar in full mode |
133
- | `closeHistory` | `() => void` | Returns to the open chat |
134
- | `toggleDebug` | `() => void` | Switches debug mode |
122
+ | Field | Type | Description |
123
+ | -------------- | --------------------- | -------------------------------------------------------------------- |
124
+ | `screen` | `'chat' \| 'threads'` | Which screen the header is on |
125
+ | `viewMode` | `'helper' \| 'full'` | The configured view mode |
126
+ | `title` | `string \| null` | The open thread's title; `null` on a new chat |
127
+ | `agentName` | `string \| undefined` | `agentName` in the widget's locale |
128
+ | `threadCount` | `number \| undefined` | How many conversations the history lists; undefined while it loads |
129
+ | `isWorking` | `boolean` | The agent is answering |
130
+ | `debug` | `{ shown, on }` | Whether Cortex offers the debug view, and whether it is on |
131
+ | `newChat` | `() => void` | Starts a new chat; absent until Cortex confirms the caller may write |
132
+ | `openHistory` | `() => void` | Shows the history: its screen, or the sidebar in full mode |
133
+ | `closeHistory` | `() => void` | Returns to the open chat |
134
+ | `toggleDebug` | `() => void` | Switches debug mode |
135
135
 
136
136
  ### `landing`
137
137
 
@@ -152,13 +152,13 @@ landing for it.
152
152
 
153
153
  The history's list. Its header is the `header` slot.
154
154
 
155
- | Field | Type | Description |
156
- | ------------------ | --------------------------------- | ----------------------------------------------------- |
157
- | `threads` | `ThreadSummary[] \| undefined` | Newest first; `undefined` while the first load is out |
158
- | `selectedThreadId` | `string \| undefined` | The open thread's id; `undefined` on a new chat |
159
- | `openThread` | `(thread: ThreadSummary) => void` | Opens a thread |
160
- | `deleteThread` | `(threadId: string) => void` | Deletes a thread |
161
- | `newChat` | `() => void` | Starts a new chat |
155
+ | Field | Type | Description |
156
+ | ------------------ | --------------------------------- | -------------------------------------------------------------------- |
157
+ | `threads` | `ThreadSummary[] \| undefined` | Newest first; `undefined` while the first load is out |
158
+ | `selectedThreadId` | `string \| undefined` | The open thread's id; `undefined` on a new chat |
159
+ | `openThread` | `(thread: ThreadSummary) => void` | Opens a thread |
160
+ | `deleteThread` | `(threadId: string) => void` | Deletes a thread; absent until Cortex confirms the caller may write |
161
+ | `newChat` | `() => void` | Starts a new chat; absent until Cortex confirms the caller may write |
162
162
 
163
163
  ## Theming
164
164
 
@@ -49,6 +49,12 @@ function checkAttachmentPolicy(file) {
49
49
  }
50
50
  var STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"];
51
51
  var APPROVAL_TITLE_FIELD = "approvalTitle";
52
+ function approvalTitleOf(input) {
53
+ if (!input || typeof input !== "object" || !(APPROVAL_TITLE_FIELD in input))
54
+ return;
55
+ const title = input[APPROVAL_TITLE_FIELD];
56
+ return typeof title === "string" && title.trim() ? title.trim() : undefined;
57
+ }
52
58
  function toolCallMetadataOf(part) {
53
59
  const metadata = part.metadata;
54
60
  return metadata?.cortex ?? undefined;
@@ -61,6 +67,8 @@ function approvalIdOf(part) {
61
67
  return part.approval?.id ?? `approval_${part.id}`;
62
68
  }
63
69
  var TIMEZONE_HEADER = "X-Cortex-Timezone";
70
+ var OPERATOR_HEADER = "X-Cortex-Operator";
71
+ var INSPECT_HEADER = "X-Cortex-Inspect";
64
72
 
65
73
  // src/internal/client/src/chat/api-client.ts
66
74
  function resolveTransportHeaders(transport) {
@@ -323,6 +331,7 @@ var ar_default = {
323
331
  translate_trigger_fired: "تم التشغيل بواسطة",
324
332
  translate_trigger_payload: "الحمولة",
325
333
  translate_messages_disabled: "يعمل هذا الوكيل من مشغّلاته فقط.",
334
+ translate_read_only: "أنت تطّلع على هذه المحادثة. لا يمكن إرسال شيء من هنا.",
326
335
  translate_access_refused: "لم يقبل هذا الوكيل تسجيل دخولك.",
327
336
  translate_agent_unreachable: "تعذّر الوصول إلى هذا الوكيل الآن. نعيد المحاولة...",
328
337
  translate_agent_missing: "هذا الوكيل غير متاح.",
@@ -477,6 +486,7 @@ var en_default = {
477
486
  translate_trigger_fired: "Triggered by",
478
487
  translate_trigger_payload: "Payload",
479
488
  translate_messages_disabled: "This agent only runs from its triggers.",
489
+ translate_read_only: "You're viewing this conversation. Nothing can be sent from here.",
480
490
  translate_access_refused: "This agent did not accept your sign-in.",
481
491
  translate_agent_unreachable: "This agent cannot be reached right now. Trying again...",
482
492
  translate_agent_missing: "This agent is not available.",
@@ -725,19 +735,12 @@ function toolCallAnimation(part) {
725
735
  rows: state === "complete" ? toolCallRows(part) : undefined
726
736
  };
727
737
  }
728
- function approvalTitle(part) {
729
- const input = part.input;
730
- if (!input || typeof input !== "object" || !(APPROVAL_TITLE_FIELD in input))
731
- return;
732
- const title = input[APPROVAL_TITLE_FIELD];
733
- return typeof title === "string" && title.trim() ? title.trim() : undefined;
734
- }
735
738
  function approvalCard(part, locale) {
736
739
  const approved = part.approval?.approved;
737
740
  const { state } = toolCallAnimation(part);
738
741
  const decision = approved === undefined ? part.state === "approval-requested" ? "pending" : "cancelled" : !approved ? "declined" : state === "error" ? "failed" : "approved";
739
742
  return {
740
- sentence: approvalTitle(part) ?? translate(locale, "translate_approval_action", { tool: wordsOf(part.name) }),
743
+ sentence: approvalTitleOf(part.input) ?? translate(locale, "translate_approval_action", { tool: wordsOf(part.name) }),
741
744
  decision,
742
745
  decisionKey: DECISION_KEYS[decision]
743
746
  };
@@ -1529,7 +1532,7 @@ function createCortexSocket(options) {
1529
1532
  resolveTransportHeaders(transport).then((headers) => {
1530
1533
  if (closed)
1531
1534
  return;
1532
- ws = new WebSocket(appendTokenToUrl(wsUrl, headers));
1535
+ ws = new WebSocket(appendAuthToUrl(wsUrl, headers));
1533
1536
  ws.addEventListener("open", () => {
1534
1537
  open = true;
1535
1538
  const opened = ws;
@@ -1588,14 +1591,21 @@ function resolveWsUrl(wsUrl, baseUrl) {
1588
1591
  parsed.pathname = parsed.pathname.replace(/\/$/, "") + "/ws";
1589
1592
  return parsed.toString();
1590
1593
  }
1591
- function appendTokenToUrl(wsUrl, headers) {
1592
- const authHeader = headers["Authorization"] ?? headers["authorization"];
1593
- if (!authHeader?.startsWith("Bearer "))
1594
+ function appendAuthToUrl(wsUrl, headers) {
1595
+ const header = (name) => Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
1596
+ const bearer = header("Authorization");
1597
+ const params = Object.entries({
1598
+ token: bearer?.startsWith("Bearer ") ? bearer.slice(7) : undefined,
1599
+ operator: header(OPERATOR_HEADER),
1600
+ inspect: header(INSPECT_HEADER)
1601
+ }).filter((entry) => Boolean(entry[1]));
1602
+ if (params.length === 0)
1594
1603
  return wsUrl;
1595
1604
  const parsed = new URL(wsUrl, window.location.origin);
1596
- if (parsed.searchParams.has("token"))
1597
- return parsed.toString();
1598
- parsed.searchParams.set("token", authHeader.slice(7));
1605
+ for (const [param, value] of params) {
1606
+ if (!parsed.searchParams.has(param))
1607
+ parsed.searchParams.set(param, value);
1608
+ }
1599
1609
  return parsed.toString();
1600
1610
  }
1601
1611
  // src/internal/client/src/chat/ask-until-answered.ts
@@ -1816,7 +1826,7 @@ function splitTail(text, consumed, runs) {
1816
1826
  if (end <= 0)
1817
1827
  break;
1818
1828
  let take = Math.min(end, run.length - consumed);
1819
- if (take < end && /[\uDC00-\uDFFF]/.test(text[end - take]))
1829
+ if (take < end && /[\uDC00-\uDFFF]/.test(text.charAt(end - take)))
1820
1830
  take += 1;
1821
1831
  const span = document.createElement("span");
1822
1832
  span.className = "cortex-stream-fade";
@@ -4562,17 +4572,19 @@ var ChatHeaderComponent = class ChatHeaderComponent2 {
4562
4572
  template: `@let header = context();
4563
4573
 
4564
4574
  <ng-template #newChatButton>
4565
- <button
4566
- type="button"
4567
- (click)="header.newChat()"
4568
- [attr.aria-label]="'translate_new_chat' | translate"
4569
- [title]="'translate_new_chat' | translate"
4570
- class="cortex-widget__header-btn cortex-widget__new-btn"
4571
- >
4572
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
4573
- <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
4574
- </svg>
4575
- </button>
4575
+ @if (header.newChat; as newChat) {
4576
+ <button
4577
+ type="button"
4578
+ (click)="newChat()"
4579
+ [attr.aria-label]="'translate_new_chat' | translate"
4580
+ [title]="'translate_new_chat' | translate"
4581
+ class="cortex-widget__header-btn cortex-widget__new-btn"
4582
+ >
4583
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
4584
+ <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
4585
+ </svg>
4586
+ </button>
4587
+ }
4576
4588
  </ng-template>
4577
4589
 
4578
4590
  @if (header.screen === 'threads') {
@@ -4701,17 +4713,19 @@ i0.ɵɵngDeclareClassMetadata({
4701
4713
  template: `@let header = context();
4702
4714
 
4703
4715
  <ng-template #newChatButton>
4704
- <button
4705
- type="button"
4706
- (click)="header.newChat()"
4707
- [attr.aria-label]="'translate_new_chat' | translate"
4708
- [title]="'translate_new_chat' | translate"
4709
- class="cortex-widget__header-btn cortex-widget__new-btn"
4710
- >
4711
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
4712
- <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
4713
- </svg>
4714
- </button>
4716
+ @if (header.newChat; as newChat) {
4717
+ <button
4718
+ type="button"
4719
+ (click)="newChat()"
4720
+ [attr.aria-label]="'translate_new_chat' | translate"
4721
+ [title]="'translate_new_chat' | translate"
4722
+ class="cortex-widget__header-btn cortex-widget__new-btn"
4723
+ >
4724
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
4725
+ <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
4726
+ </svg>
4727
+ </button>
4728
+ }
4715
4729
  </ng-template>
4716
4730
 
4717
4731
  @if (header.screen === 'threads') {
@@ -9694,9 +9708,10 @@ var MessageToolCallAnimatedComponent = class MessageToolCallAnimatedComponent2 {
9694
9708
  } : null;
9695
9709
  }, ...ngDevMode ? [{ debugName: "mountedComponent" }] : []);
9696
9710
  clientToolFallback = signal(false, ...ngDevMode ? [{ debugName: "clientToolFallback" }] : []);
9697
- owed = computed(() => this.toolCallPart().state === "input-complete" && !this.chatService.isAgentWorking(), ...ngDevMode ? [{ debugName: "owed" }] : []);
9711
+ answers = computed(() => this.chatService.capabilities()?.messages === true, ...ngDevMode ? [{ debugName: "answers" }] : []);
9712
+ owed = computed(() => this.answers() && this.toolCallPart().state === "input-complete" && !this.chatService.isAgentWorking(), ...ngDevMode ? [{ debugName: "owed" }] : []);
9698
9713
  showClientTool = computed(() => !this.clientToolFallback() && this.owed() && toolCallMetadataOf(this.toolCallPart())?.embed !== undefined, ...ngDevMode ? [{ debugName: "showClientTool" }] : []);
9699
- approvalRequested = computed(() => this.toolCallPart().state === "approval-requested" && !this.chatService.isAgentWorking(), ...ngDevMode ? [{ debugName: "approvalRequested" }] : []);
9714
+ approvalRequested = computed(() => this.answers() && this.toolCallPart().state === "approval-requested" && !this.chatService.isAgentWorking(), ...ngDevMode ? [{ debugName: "approvalRequested" }] : []);
9700
9715
  animation = computed(() => toolCallAnimation(this.toolCallPart()), ...ngDevMode ? [{ debugName: "animation" }] : []);
9701
9716
  animState = computed(() => this.animation().state, ...ngDevMode ? [{ debugName: "animState" }] : []);
9702
9717
  gated = computed(() => this.animation().gated, ...ngDevMode ? [{ debugName: "gated" }] : []);
@@ -11626,7 +11641,7 @@ var MessageListComponent = class MessageListComponent2 {
11626
11641
  <cortex-message-aborted-flag [label]="failureLabel(failure)" />
11627
11642
  <!-- The reason may name the model provider's answer: shown only where
11628
11643
  the debug view is offered. -->
11629
- @if (config().showDebugButton && failure.reason) {
11644
+ @if (chatService.capabilities()?.debug && failure.reason) {
11630
11645
  <p class="cortex-aborted-flag__reason">{{ failure.reason }}</p>
11631
11646
  }
11632
11647
  }
@@ -11722,7 +11737,7 @@ i0.ɵɵngDeclareClassMetadata({
11722
11737
  <cortex-message-aborted-flag [label]="failureLabel(failure)" />
11723
11738
  <!-- The reason may name the model provider's answer: shown only where
11724
11739
  the debug view is offered. -->
11725
- @if (config().showDebugButton && failure.reason) {
11740
+ @if (chatService.capabilities()?.debug && failure.reason) {
11726
11741
  <p class="cortex-aborted-flag__reason">{{ failure.reason }}</p>
11727
11742
  }
11728
11743
  }
@@ -11949,22 +11964,24 @@ var ThreadListComponent = class ThreadListComponent2 {
11949
11964
  }
11950
11965
  </button>
11951
11966
 
11952
- <button
11953
- type="button"
11954
- [attr.aria-label]="'translate_delete_conversation' | translate"
11955
- (click)="list.deleteThread(thread.id)"
11956
- class="cortex-widget__thread-delete"
11957
- [class.cortex-widget__thread-delete--active]="isActive"
11958
- >
11959
- <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
11960
- <path
11961
- d="M4 4l8 8M12 4l-8 8"
11962
- stroke="currentColor"
11963
- stroke-width="1.3"
11964
- stroke-linecap="round"
11965
- />
11966
- </svg>
11967
- </button>
11967
+ @if (list.deleteThread; as deleteThread) {
11968
+ <button
11969
+ type="button"
11970
+ [attr.aria-label]="'translate_delete_conversation' | translate"
11971
+ (click)="deleteThread(thread.id)"
11972
+ class="cortex-widget__thread-delete"
11973
+ [class.cortex-widget__thread-delete--active]="isActive"
11974
+ >
11975
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
11976
+ <path
11977
+ d="M4 4l8 8M12 4l-8 8"
11978
+ stroke="currentColor"
11979
+ stroke-width="1.3"
11980
+ stroke-linecap="round"
11981
+ />
11982
+ </svg>
11983
+ </button>
11984
+ }
11968
11985
 
11969
11986
  <svg
11970
11987
  class="cortex-widget__thread-arrow"
@@ -12010,12 +12027,14 @@ var ThreadListComponent = class ThreadListComponent2 {
12010
12027
  <p class="cortex-widget__threads-empty-subtitle">
12011
12028
  {{ 'translate_start_a_new_conversation' | translate }}
12012
12029
  </p>
12013
- <button
12014
- (click)="list.newChat()"
12015
- class="cortex-widget__new-chat-btn cortex-widget__new-chat-btn--empty-state"
12016
- >
12017
- {{ 'translate_new_chat' | translate }}
12018
- </button>
12030
+ @if (list.newChat; as newChat) {
12031
+ <button
12032
+ (click)="newChat()"
12033
+ class="cortex-widget__new-chat-btn cortex-widget__new-chat-btn--empty-state"
12034
+ >
12035
+ {{ 'translate_new_chat' | translate }}
12036
+ </button>
12037
+ }
12019
12038
  </div>
12020
12039
  }
12021
12040
  `,
@@ -12105,22 +12124,24 @@ i0.ɵɵngDeclareClassMetadata({
12105
12124
  }
12106
12125
  </button>
12107
12126
 
12108
- <button
12109
- type="button"
12110
- [attr.aria-label]="'translate_delete_conversation' | translate"
12111
- (click)="list.deleteThread(thread.id)"
12112
- class="cortex-widget__thread-delete"
12113
- [class.cortex-widget__thread-delete--active]="isActive"
12114
- >
12115
- <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
12116
- <path
12117
- d="M4 4l8 8M12 4l-8 8"
12118
- stroke="currentColor"
12119
- stroke-width="1.3"
12120
- stroke-linecap="round"
12121
- />
12122
- </svg>
12123
- </button>
12127
+ @if (list.deleteThread; as deleteThread) {
12128
+ <button
12129
+ type="button"
12130
+ [attr.aria-label]="'translate_delete_conversation' | translate"
12131
+ (click)="deleteThread(thread.id)"
12132
+ class="cortex-widget__thread-delete"
12133
+ [class.cortex-widget__thread-delete--active]="isActive"
12134
+ >
12135
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
12136
+ <path
12137
+ d="M4 4l8 8M12 4l-8 8"
12138
+ stroke="currentColor"
12139
+ stroke-width="1.3"
12140
+ stroke-linecap="round"
12141
+ />
12142
+ </svg>
12143
+ </button>
12144
+ }
12124
12145
 
12125
12146
  <svg
12126
12147
  class="cortex-widget__thread-arrow"
@@ -12166,12 +12187,14 @@ i0.ɵɵngDeclareClassMetadata({
12166
12187
  <p class="cortex-widget__threads-empty-subtitle">
12167
12188
  {{ 'translate_start_a_new_conversation' | translate }}
12168
12189
  </p>
12169
- <button
12170
- (click)="list.newChat()"
12171
- class="cortex-widget__new-chat-btn cortex-widget__new-chat-btn--empty-state"
12172
- >
12173
- {{ 'translate_new_chat' | translate }}
12174
- </button>
12190
+ @if (list.newChat; as newChat) {
12191
+ <button
12192
+ (click)="newChat()"
12193
+ class="cortex-widget__new-chat-btn cortex-widget__new-chat-btn--empty-state"
12194
+ >
12195
+ {{ 'translate_new_chat' | translate }}
12196
+ </button>
12197
+ }
12175
12198
  </div>
12176
12199
  }
12177
12200
  `
@@ -12243,6 +12266,7 @@ i0.ɵɵngDeclareClassMetadata({
12243
12266
  var CortexChatWidgetComponent = class CortexChatWidgetComponent2 {
12244
12267
  config = input.required(...ngDevMode ? [{ debugName: "config" }] : []);
12245
12268
  debugMode = signal(false, ...ngDevMode ? [{ debugName: "debugMode" }] : []);
12269
+ writes = computed(() => this.chatService.capabilities()?.readOnly === false, ...ngDevMode ? [{ debugName: "writes" }] : []);
12246
12270
  screen = signal("chat", ...ngDevMode ? [{ debugName: "screen" }] : []);
12247
12271
  sidebarOpen = signal(false, ...ngDevMode ? [{ debugName: "sidebarOpen" }] : []);
12248
12272
  viewMode = computed(() => this.config().viewMode ?? "helper", ...ngDevMode ? [{ debugName: "viewMode" }] : []);
@@ -12270,8 +12294,10 @@ var CortexChatWidgetComponent = class CortexChatWidgetComponent2 {
12270
12294
  threads: this.threadService.threads(),
12271
12295
  selectedThreadId: this.chatService.selectedThread()?.id,
12272
12296
  openThread: (thread) => this.selectThread(thread),
12273
- deleteThread: (threadId) => this.chatService.deleteThread(threadId).subscribe(),
12274
- newChat: () => this.newChat()
12297
+ ...this.writes() && {
12298
+ deleteThread: (threadId) => this.chatService.deleteThread(threadId).subscribe(),
12299
+ newChat: () => this.newChat()
12300
+ }
12275
12301
  }), ...ngDevMode ? [{ debugName: "threadList" }] : []);
12276
12302
  chatService;
12277
12303
  queue;
@@ -12291,6 +12317,15 @@ var CortexChatWidgetComponent = class CortexChatWidgetComponent2 {
12291
12317
  this.queue.store.clear(selectedThreadId);
12292
12318
  selectedThreadId = nextThreadId;
12293
12319
  }, { injector: this.injector });
12320
+ let openedConfigThread = false;
12321
+ effect(() => {
12322
+ const id = this.config().thread;
12323
+ const named = id && this.threadService.threads()?.find((thread) => thread.id === id);
12324
+ if (openedConfigThread || !named)
12325
+ return;
12326
+ openedConfigThread = true;
12327
+ untracked(() => this.selectThread(named));
12328
+ }, { injector: this.injector });
12294
12329
  this.chatService.events$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
12295
12330
  if (event === "onError") {
12296
12331
  this.queue.store.restoreConsumed(this.chatService.selectedThread()?.id);
@@ -12313,10 +12348,10 @@ var CortexChatWidgetComponent = class CortexChatWidgetComponent2 {
12313
12348
  threadCount: this.threadService.threads()?.length,
12314
12349
  isWorking: this.chatService.isAgentWorking(),
12315
12350
  debug: {
12316
- shown: Boolean(this.config().showDebugButton),
12351
+ shown: Boolean(this.chatService.capabilities()?.debug),
12317
12352
  on: this.debugMode()
12318
12353
  },
12319
- newChat: () => this.newChat(),
12354
+ ...this.writes() && { newChat: () => this.newChat() },
12320
12355
  openHistory: () => this.openHistory(),
12321
12356
  closeHistory: () => this.closeHistory(),
12322
12357
  toggleDebug: () => this.toggleDebugMode()
@@ -12507,7 +12542,12 @@ var CortexChatWidgetComponent = class CortexChatWidgetComponent2 {
12507
12542
  </p>
12508
12543
  } @else if (chatService.capabilities()?.messages === false) {
12509
12544
  <p class="cortex-widget__messages-disabled">
12510
- {{ 'translate_messages_disabled' | translate }}
12545
+ {{
12546
+ (chatService.capabilities()?.readOnly
12547
+ ? 'translate_read_only'
12548
+ : 'translate_messages_disabled'
12549
+ ) | translate
12550
+ }}
12511
12551
  </p>
12512
12552
  } @else if (chatService.capabilities() !== undefined) {
12513
12553
  <cortex-chat-composer />
@@ -12708,7 +12748,12 @@ i0.ɵɵngDeclareClassMetadata({
12708
12748
  </p>
12709
12749
  } @else if (chatService.capabilities()?.messages === false) {
12710
12750
  <p class="cortex-widget__messages-disabled">
12711
- {{ 'translate_messages_disabled' | translate }}
12751
+ {{
12752
+ (chatService.capabilities()?.readOnly
12753
+ ? 'translate_read_only'
12754
+ : 'translate_messages_disabled'
12755
+ ) | translate
12756
+ }}
12712
12757
  </p>
12713
12758
  } @else if (chatService.capabilities() !== undefined) {
12714
12759
  <cortex-chat-composer />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-angular",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Angular chat widget for Cortex agents",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -61,12 +61,23 @@ export type CreateThreadRequest = {
61
61
  */
62
62
  export type AgentCapabilities = {
63
63
  attachments: boolean;
64
- /** False when the agent's `message` trigger is off: the widget hides its composer. */
64
+ /**
65
+ * False when this caller may not send to the chat: the agent's `message`
66
+ * trigger is off, or the caller only reads (`readOnly`). The widget hides
67
+ * its composer and answers no tool call.
68
+ */
65
69
  messages: boolean;
70
+ /** True when the caller only reads, as an operator inspecting a user's thread does. */
71
+ readOnly: boolean;
66
72
  /** Prompt chips on the blank-chat landing per locale; clicking one sends its `prompt` as the first message. */
67
73
  suggestions: Suggestions;
68
74
  /** Headline of the blank-chat landing per locale; "" falls back to the widget's built-in copy. */
69
75
  greeting: Greeting;
76
+ /**
77
+ * Whether this caller gets the debug view: tool inputs and outputs, reasoning,
78
+ * model calls. Cortex sends anyone else only what the widget's normal UI draws.
79
+ */
80
+ debug: boolean;
70
81
  };
71
82
  /**
72
83
  * One landing-page chip: the widget shows `title` and sends `prompt` as the
@@ -165,7 +176,11 @@ export type CortexConfig = {
165
176
  * or a map keyed by locale (`{ en: 'Support', ar: 'الدعم' }`).
166
177
  */
167
178
  agentName?: Localized<string>;
168
- showDebugButton?: boolean;
179
+ /**
180
+ * A thread to open when the widget mounts, by id, once the thread list holds
181
+ * it: a deep link into one conversation, such as the console's inspector.
182
+ */
183
+ thread?: string;
169
184
  /**
170
185
  * Render embedded tool pages served from this page's own origin with their
171
186
  * full identity — which, same-origin, means no effective sandbox at all: such
@@ -193,12 +208,13 @@ export type CortexHeaderContext = {
193
208
  /** How many conversations the history lists; undefined while the first load is out. */
194
209
  threadCount: number | undefined;
195
210
  isWorking: boolean;
196
- /** Whether the consumer enabled the debug switch, and whether it is on. */
211
+ /** Whether Cortex offers this caller the debug view, and whether it is on. */
197
212
  debug: {
198
213
  shown: boolean;
199
214
  on: boolean;
200
215
  };
201
- newChat: () => void;
216
+ /** Absent until the handshake says the caller may write; an operator inspecting a thread only reads. */
217
+ newChat?: () => void;
202
218
  /** Shows the history: the history screen, or in full mode the sidebar where it is hidden. */
203
219
  openHistory: () => void;
204
220
  /** Returns to the open chat from wherever `openHistory` went. */
@@ -236,8 +252,9 @@ export type CortexThreadListContext = {
236
252
  /** The open thread's id; undefined on a new chat. */
237
253
  selectedThreadId: string | undefined;
238
254
  openThread: (thread: ThreadSummary) => void;
239
- deleteThread: (threadId: string) => void;
240
- newChat: () => void;
255
+ /** Both absent until the handshake says the caller may write; an operator inspecting a thread only reads. */
256
+ deleteThread?: (threadId: string) => void;
257
+ newChat?: () => void;
241
258
  };
242
259
  /** Each part of the widget a consumer can draw itself, with the context it receives. */
243
260
  export type CortexSlotContexts<View> = {
@@ -442,6 +459,12 @@ declare class CortexChatService {
442
459
  export declare class CortexChatWidgetComponent implements OnInit {
443
460
  readonly config: import("@angular/core").InputSignal<CortexAngularConfig>;
444
461
  protected readonly debugMode: import("@angular/core").WritableSignal<boolean>;
462
+ /**
463
+ * Whether this caller may start and delete conversations: only once the
464
+ * handshake says so, like every capability-gated control, and never for a
465
+ * caller that only reads, as an operator inspecting a thread does.
466
+ */
467
+ private readonly writes;
445
468
  protected readonly screen: import("@angular/core").WritableSignal<"chat" | "threads">;
446
469
  protected readonly sidebarOpen: import("@angular/core").WritableSignal<boolean>;
447
470
  protected readonly viewMode: import("@angular/core").Signal<CortexViewMode>;
@@ -455,6 +478,10 @@ export declare class CortexChatWidgetComponent implements OnInit {
455
478
  /** Why the handshake has not answered, worded for the composer's place. */
456
479
  protected readonly unansweredNotice: import("@angular/core").Signal<"translate_access_refused" | "translate_agent_unreachable" | "translate_agent_missing" | undefined>;
457
480
  protected readonly chatHeader: import("@angular/core").Signal<{
481
+ openHistory: () => void;
482
+ closeHistory: () => void;
483
+ toggleDebug: () => void;
484
+ newChat?: (() => void) | undefined;
458
485
  screen: "chat" | "threads";
459
486
  viewMode: CortexViewMode;
460
487
  title: string | null;
@@ -465,12 +492,12 @@ export declare class CortexChatWidgetComponent implements OnInit {
465
492
  shown: boolean;
466
493
  on: boolean;
467
494
  };
468
- newChat: () => void;
495
+ }>;
496
+ protected readonly threadsHeader: import("@angular/core").Signal<{
469
497
  openHistory: () => void;
470
498
  closeHistory: () => void;
471
499
  toggleDebug: () => void;
472
- }>;
473
- protected readonly threadsHeader: import("@angular/core").Signal<{
500
+ newChat?: (() => void) | undefined;
474
501
  screen: "chat" | "threads";
475
502
  viewMode: CortexViewMode;
476
503
  title: string | null;
@@ -481,10 +508,6 @@ export declare class CortexChatWidgetComponent implements OnInit {
481
508
  shown: boolean;
482
509
  on: boolean;
483
510
  };
484
- newChat: () => void;
485
- openHistory: () => void;
486
- closeHistory: () => void;
487
- toggleDebug: () => void;
488
511
  }>;
489
512
  protected readonly landing: import("@angular/core").Signal<{
490
513
  threads: ThreadSummary[] | undefined;
@@ -496,11 +519,11 @@ export declare class CortexChatWidgetComponent implements OnInit {
496
519
  send: (prompt: string) => void;
497
520
  }>;
498
521
  protected readonly threadList: import("@angular/core").Signal<{
522
+ deleteThread?: ((threadId: string) => import("rxjs").Subscription) | undefined;
523
+ newChat?: (() => void) | undefined;
499
524
  threads: ThreadSummary[] | undefined;
500
525
  selectedThreadId: string | undefined;
501
526
  openThread: (thread: ThreadSummary) => void;
502
- deleteThread: (threadId: string) => import("rxjs").Subscription;
503
- newChat: () => void;
504
527
  }>;
505
528
  protected chatService: CortexChatService;
506
529
  protected queue: AttachmentQueueService;