@things-factory/ai-assistant 10.1.154 → 10.1.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/client/bootstrap.ts +27 -0
  2. package/client/components/assistant-chat.ts +229 -178
  3. package/client/components/assistant-dock.ts +53 -48
  4. package/client/components/assistant-session-manager.ts +82 -100
  5. package/client/components/assistant-session-toolbar.ts +11 -3
  6. package/client/components/chat-input-builder.ts +8 -2
  7. package/client/settings/agent-settings-model.ts +164 -0
  8. package/client/settings/ai-agent-setting-let.ts +684 -0
  9. package/client/settings/ai-usage-setting-let.ts +252 -0
  10. package/client/settings/usage-model.ts +46 -0
  11. package/client/setup-assistant-dock.ts +2 -3
  12. package/client/utils/agent-choice.ts +56 -0
  13. package/client/utils/assistant-navigation.test.ts +14 -8
  14. package/client/utils/assistant-navigation.ts +8 -3
  15. package/client/utils/composer-frame.ts +28 -2
  16. package/client/utils/menu-room.ts +33 -0
  17. package/dist-client/bootstrap.d.ts +8 -0
  18. package/dist-client/bootstrap.js +26 -0
  19. package/dist-client/bootstrap.js.map +1 -0
  20. package/dist-client/components/assistant-chat.d.ts +32 -4
  21. package/dist-client/components/assistant-chat.js +234 -175
  22. package/dist-client/components/assistant-chat.js.map +1 -1
  23. package/dist-client/components/assistant-dock.d.ts +6 -8
  24. package/dist-client/components/assistant-dock.js +52 -60
  25. package/dist-client/components/assistant-dock.js.map +1 -1
  26. package/dist-client/components/assistant-session-manager.d.ts +8 -7
  27. package/dist-client/components/assistant-session-manager.js +84 -124
  28. package/dist-client/components/assistant-session-manager.js.map +1 -1
  29. package/dist-client/components/assistant-session-toolbar.d.ts +1 -0
  30. package/dist-client/components/assistant-session-toolbar.js +10 -3
  31. package/dist-client/components/assistant-session-toolbar.js.map +1 -1
  32. package/dist-client/components/chat-input-builder.d.ts +4 -0
  33. package/dist-client/components/chat-input-builder.js +4 -2
  34. package/dist-client/components/chat-input-builder.js.map +1 -1
  35. package/dist-client/settings/agent-settings-model.d.ts +93 -0
  36. package/dist-client/settings/agent-settings-model.js +95 -0
  37. package/dist-client/settings/agent-settings-model.js.map +1 -0
  38. package/dist-client/settings/ai-agent-setting-let.d.ts +51 -0
  39. package/dist-client/settings/ai-agent-setting-let.js +672 -0
  40. package/dist-client/settings/ai-agent-setting-let.js.map +1 -0
  41. package/dist-client/settings/ai-usage-setting-let.d.ts +26 -0
  42. package/dist-client/settings/ai-usage-setting-let.js +276 -0
  43. package/dist-client/settings/ai-usage-setting-let.js.map +1 -0
  44. package/dist-client/settings/usage-model.d.ts +32 -0
  45. package/dist-client/settings/usage-model.js +25 -0
  46. package/dist-client/settings/usage-model.js.map +1 -0
  47. package/dist-client/setup-assistant-dock.d.ts +0 -1
  48. package/dist-client/setup-assistant-dock.js +0 -1
  49. package/dist-client/setup-assistant-dock.js.map +1 -1
  50. package/dist-client/tsconfig.tsbuildinfo +1 -1
  51. package/dist-client/utils/agent-choice.d.ts +24 -0
  52. package/dist-client/utils/agent-choice.js +38 -0
  53. package/dist-client/utils/agent-choice.js.map +1 -0
  54. package/dist-client/utils/assistant-navigation.d.ts +8 -3
  55. package/dist-client/utils/assistant-navigation.js +7 -2
  56. package/dist-client/utils/assistant-navigation.js.map +1 -1
  57. package/dist-client/utils/assistant-navigation.test.js +14 -8
  58. package/dist-client/utils/assistant-navigation.test.js.map +1 -1
  59. package/dist-client/utils/composer-frame.d.ts +23 -2
  60. package/dist-client/utils/composer-frame.js +20 -2
  61. package/dist-client/utils/composer-frame.js.map +1 -1
  62. package/dist-client/utils/menu-room.d.ts +13 -0
  63. package/dist-client/utils/menu-room.js +26 -0
  64. package/dist-client/utils/menu-room.js.map +1 -0
  65. package/dist-server/service/ai-agent-resolver.d.ts +82 -0
  66. package/dist-server/service/ai-agent-resolver.js +417 -0
  67. package/dist-server/service/ai-agent-resolver.js.map +1 -0
  68. package/dist-server/service/assistant-capability-resolver.d.ts +1 -1
  69. package/dist-server/service/assistant-capability-resolver.js +7 -7
  70. package/dist-server/service/assistant-capability-resolver.js.map +1 -1
  71. package/dist-server/service/assistant-chat-resolver.d.ts +2 -0
  72. package/dist-server/service/assistant-chat-resolver.js +32 -15
  73. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  74. package/dist-server/service/assistant-mode/resolve.d.ts +0 -1
  75. package/dist-server/service/assistant-mode/resolve.js +0 -1
  76. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  77. package/dist-server/service/assistant-mode/types.d.ts +1 -2
  78. package/dist-server/service/assistant-mode/types.js.map +1 -1
  79. package/dist-server/service/chat-session/chat-session.d.ts +2 -0
  80. package/dist-server/service/chat-session/chat-session.js +5 -0
  81. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  82. package/dist-server/service/index.d.ts +2 -1
  83. package/dist-server/service/index.js +3 -0
  84. package/dist-server/service/index.js.map +1 -1
  85. package/dist-server/service/testing/mode-conformance.js +0 -1
  86. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  87. package/dist-server/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +9 -7
  89. package/server/service/ai-agent-resolver.ts +330 -0
  90. package/server/service/assistant-capability-resolver.ts +7 -7
  91. package/server/service/assistant-chat-resolver.ts +32 -24
  92. package/server/service/assistant-mode/resolve.test.ts +0 -2
  93. package/server/service/assistant-mode/resolve.ts +0 -2
  94. package/server/service/assistant-mode/types.ts +1 -2
  95. package/server/service/chat-session/chat-session.ts +5 -0
  96. package/server/service/index.ts +3 -0
  97. package/server/service/testing/mode-conformance.ts +0 -1
  98. package/server/tool-registry-boot-report.test.ts +2 -0
  99. package/test/ai-agent-settings.test.ts +123 -0
  100. package/test/assistant-dock.test.ts +3 -2
  101. package/test/assistant-session-manager.test.ts +10 -4
  102. package/test/assistant-session-toolbar.test.ts +8 -0
  103. package/test/composer-frame.test.ts +40 -2
  104. package/test/menu-room.test.ts +33 -0
  105. package/test/open-doors.test.ts +3 -1
  106. package/test/session-discovery.test.ts +7 -6
  107. package/things-factory.config.js +2 -0
  108. package/translations/en.json +78 -12
  109. package/translations/es.json +78 -12
  110. package/translations/ja.json +78 -12
  111. package/translations/ko.json +78 -12
  112. package/translations/ms.json +78 -12
  113. package/translations/zh.json +78 -12
  114. package/client/components/chat-triggers.test.ts +0 -72
  115. package/client/components/chat-triggers.ts +0 -62
  116. package/dist-client/components/chat-triggers.d.ts +0 -16
  117. package/dist-client/components/chat-triggers.js +0 -42
  118. package/dist-client/components/chat-triggers.js.map +0 -1
  119. package/dist-client/components/chat-triggers.test.d.ts +0 -1
  120. package/dist-client/components/chat-triggers.test.js +0 -61
  121. package/dist-client/components/chat-triggers.test.js.map +0 -1
@@ -31,10 +31,12 @@ import gql from 'graphql-tag';
31
31
  import { renderMarkdown, injectMentionRefids, injectMentionUserIds, stripMentionRefids } from './markdown.js';
32
32
  import { acceptOf, chatAttachmentUrl, refuseAttachments, textCharsOf } from '../utils/attachment-policy.js';
33
33
  import { pastedTableFile } from '../utils/pasted-table.js';
34
- import { appendWords, composerHeight, composerKeyAction, sendShortcut, tableAsWords } from '../utils/composer-frame.js';
34
+ import { appendWords, composerHeight, composerKeyAction, composerSuggestion, sendShortcut, tableAsWords } from '../utils/composer-frame.js';
35
35
  import { readSpreadsheet, spreadsheetKindOf } from '../utils/spreadsheet-text.js';
36
36
  import { entryReviewAddress, offersWorkspaceReview } from '../utils/entry-workspace.js';
37
37
  import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js';
38
+ import { agentDetail, offersAgentPicker, pickAgentId, rememberAgent, rememberedAgent } from '../utils/agent-choice.js';
39
+ import { store } from '@operato/shell';
38
40
  import { receiveRemoteMessage, reconcileSentLine, remoteMessageToLine } from './chat-echo-dedup.js';
39
41
  import './mention-popup.js';
40
42
  import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, buildUserCandidates, extractMentionContext, filterMentionCandidates, applyMentionSelection, resolveMentionToken } from './mention-popup.js';
@@ -52,7 +54,6 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
52
54
  import { chatDefaults } from './chat-defaults.js';
53
55
  import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode.js';
54
56
  import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
55
- import { availableTriggers } from './chat-triggers.js';
56
57
  import './entry-review.js';
57
58
  import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
58
59
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
@@ -139,6 +140,17 @@ const ASSISTANT_MODES_QUERY = gql `
139
140
  }
140
141
  }
141
142
  `;
143
+ const AI_AGENT_CHOICES_QUERY = gql `
144
+ query AiAgentChoices {
145
+ aiAgentChoices {
146
+ id
147
+ name
148
+ provider
149
+ model
150
+ isDefault
151
+ }
152
+ }
153
+ `;
142
154
  const ASSISTANT_MODE_QUERY = gql `
143
155
  query AssistantMode($kind: String!) {
144
156
  assistantMode(kind: $kind) {
@@ -345,6 +357,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
345
357
  /** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
346
358
  this.modeChoices = [];
347
359
  this.modeChoicesToken = 0;
360
+ /** The AI agents this domain lets a person pick (design/ai/agent-settings.md §5). One or none draws no picker. */
361
+ this.agentChoices = [];
362
+ this.agentChoicesToken = 0;
348
363
  /*
349
364
  * The `@` and `/` candidates, built once per mode.
350
365
  *
@@ -660,6 +675,22 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
660
675
  get navigationState() {
661
676
  return { busy: this.busy, draft: !!this.input.trim() || this.attachments.length > 0 };
662
677
  }
678
+ /**
679
+ * What is being written, handed to the host when the page moves to another subject - it belongs to the conversation
680
+ * it was written in, and comes back with `restoreDraft` when the person returns. The box is left empty.
681
+ */
682
+ takeDraft() {
683
+ if (!this.input.trim() && !this.attachments.length)
684
+ return undefined;
685
+ const draft = { text: this.input, attachments: this.attachments };
686
+ this.input = '';
687
+ this.attachments = [];
688
+ return draft;
689
+ }
690
+ restoreDraft(draft) {
691
+ this.input = draft.text;
692
+ this.attachments = draft.attachments;
693
+ }
663
694
  /** 현재 로그인 사용자 email — 그룹챗에서 내 메시지 vs 타인 메시지 구분.
664
695
  * email 을 쓰는 이유: /auth/profile 이 항상 내려주는 로그인 식별자이고(비어 있지 않다),
665
696
  * 내부 관리키(User.id)를 클라이언트 판정에 의존하지 않기 위함. */
@@ -685,6 +716,35 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
685
716
  stagedNotice: chatDefaults.stagedNoticeKey ? i18next.t(chatDefaults.stagedNoticeKey) : undefined
686
717
  }, chatDefaults, this.modeView);
687
718
  }
719
+ get contextPath() {
720
+ return store.getState()?.app?.contextPath;
721
+ }
722
+ /**
723
+ * Asks which agents this domain offers. The list changes when someone edits the domain's agents, so it is read
724
+ * again whenever the conversation's subject is - never cached for the life of the page.
725
+ */
726
+ async loadAgentChoices() {
727
+ const token = ++this.agentChoicesToken;
728
+ try {
729
+ const result = await client.query({ query: AI_AGENT_CHOICES_QUERY, fetchPolicy: 'network-only' });
730
+ if (token !== this.agentChoicesToken)
731
+ return;
732
+ this.agentChoices = result.data?.aiAgentChoices ?? [];
733
+ }
734
+ catch {
735
+ /* Not knowing the list leaves the domain default to answer - the picker just does not appear. */
736
+ if (token !== this.agentChoicesToken)
737
+ return;
738
+ this.agentChoices = [];
739
+ }
740
+ this.agentId = pickAgentId(this.agentChoices, rememberedAgent(this.contextPath));
741
+ }
742
+ onAgentPick(agentId) {
743
+ if (!this.agentChoices.some(one => one.id === agentId))
744
+ return;
745
+ this.agentId = agentId;
746
+ rememberAgent(this.contextPath, agentId);
747
+ }
688
748
  /**
689
749
  * 이 화면에 설 수 있는 대화를 서버에 묻는다.
690
750
  *
@@ -853,6 +913,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
853
913
  padding: 2px 10px;
854
914
  white-space: nowrap;
855
915
  }
916
+ /* The agent sits at the end of the row above the box. */
917
+ .agent-select {
918
+ margin-left: auto;
919
+ max-width: 50%;
920
+ text-overflow: ellipsis;
921
+ }
856
922
  .mode-switch:hover:not([disabled]) {
857
923
  background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent);
858
924
  color: var(--primary-color, #007f87);
@@ -1089,77 +1155,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
1089
1155
  line-height: 1.6;
1090
1156
  text-align: center;
1091
1157
  }
1092
- .empty .footer .badge {
1093
- display: inline-block;
1094
- margin: 0 4px;
1095
- padding: 1px 7px;
1096
- background: var(--md-sys-color-surface-container, #f8fafc);
1097
- border-radius: 8px;
1098
- font-size: 9.5px;
1099
- color: var(--md-sys-color-on-surface-variant, #64748b);
1100
- }
1101
- /* ── Trigger chip row (empty state) ──────────────── */
1102
- .empty .trigger-row {
1103
- margin: 12px 0 4px;
1104
- padding: 10px 12px;
1105
- background: var(--md-sys-color-surface-container-low, #f8fafc);
1106
- border: 1px solid var(--md-sys-color-outline-variant, #eef2f6);
1107
- border-radius: 10px;
1108
- }
1109
- .empty .trigger-row-label {
1110
- font-size: 10px;
1111
- color: var(--md-sys-color-on-surface-variant, #64748b);
1112
- margin-bottom: 6px;
1113
- text-align: center;
1114
- letter-spacing: 0.02em;
1115
- }
1116
- .empty .trigger-chips {
1117
- display: flex;
1118
- gap: 6px;
1119
- justify-content: center;
1120
- flex-wrap: wrap;
1121
- }
1122
- .empty .trigger-chip {
1123
- display: inline-flex;
1124
- align-items: center;
1125
- gap: 5px;
1126
- padding: 5px 10px;
1127
- background: var(--md-sys-color-surface, #ffffff);
1128
- border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
1129
- border-radius: 18px;
1130
- cursor: pointer;
1131
- font-size: 11.5px;
1132
- color: var(--md-sys-color-on-surface, #0f172a);
1133
- transition:
1134
- border-color 0.15s,
1135
- background 0.15s,
1136
- color 0.15s;
1137
- }
1138
- .empty .trigger-chip:hover {
1139
- border-color: var(--md-sys-color-primary, #2563eb);
1140
- background: var(--md-sys-color-primary-container, #dbeafe);
1141
- color: var(--md-sys-color-on-primary-container, #1e3a8a);
1142
- }
1143
- .empty .trigger-chip .trigger-char {
1144
- font-family:
1145
- 'SF Mono',
1146
- Menlo,
1147
- Monaco,
1148
- Consolas,
1149
- monospace;
1150
- font-size: 13px;
1151
- font-weight: 600;
1152
- color: var(--md-sys-color-primary, #2563eb);
1153
- min-width: 12px;
1154
- text-align: center;
1155
- }
1156
- .empty .trigger-chip:hover .trigger-char {
1157
- color: var(--md-sys-color-on-primary-container, #1e3a8a);
1158
- }
1159
- .empty .trigger-chip .trigger-desc {
1160
- font-weight: 500;
1161
- }
1162
-
1163
1158
  /* ── Message label (sender hint) ──────────────────── */
1164
1159
  .msg-label {
1165
1160
  font-size: 9.5px;
@@ -2077,9 +2072,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2077
2072
  .composer.wide textarea {
2078
2073
  min-height: 0;
2079
2074
  }
2080
- .composer-size {
2081
- margin-left: auto;
2082
- }
2083
2075
  /* Named above the send button, out of the row - it takes no width from the box. */
2084
2076
  .composer.wide .input-row {
2085
2077
  position: relative;
@@ -2087,7 +2079,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2087
2079
  .send-shortcut {
2088
2080
  position: absolute;
2089
2081
  right: 14px;
2090
- bottom: 48px;
2082
+ bottom: 68px;
2091
2083
  font-size: 10px;
2092
2084
  color: var(--md-sys-color-outline, #64748b);
2093
2085
  white-space: nowrap;
@@ -2366,6 +2358,34 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2366
2358
  opacity: 0.7;
2367
2359
  }
2368
2360
 
2361
+ /* Low-key words in the row above the box - examples, write wide. Text, no frame (page-standard.md §8 quiet). */
2362
+ .quiet {
2363
+ all: unset;
2364
+ cursor: pointer;
2365
+ font-size: 11px;
2366
+ line-height: 20px;
2367
+ color: var(--md-sys-color-on-surface-variant, #475569);
2368
+ }
2369
+ .quiet:hover,
2370
+ .quiet[aria-expanded='true'],
2371
+ .quiet[aria-pressed='true'] {
2372
+ color: var(--md-sys-color-primary, #0f172a);
2373
+ text-decoration: underline;
2374
+ text-underline-offset: 2px;
2375
+ }
2376
+ /* The words in the row read as separate controls only with a mark between them. */
2377
+ .quiet-sep {
2378
+ font-size: 11px;
2379
+ line-height: 20px;
2380
+ color: var(--md-sys-color-outline, #94a3b8);
2381
+ user-select: none;
2382
+ }
2383
+ .quiet:focus-visible {
2384
+ outline: 2px solid var(--md-sys-color-primary, #0f172a);
2385
+ outline-offset: 2px;
2386
+ border-radius: 2px;
2387
+ }
2388
+
2369
2389
  /* ── Inline examples (action row 의 "예시" 토글 시) ──── */
2370
2390
  .inline-examples {
2371
2391
  padding: 4px 16px 0;
@@ -2413,11 +2433,32 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2413
2433
  .reply-preview strong { display: block; font-size: 12px; }
2414
2434
  .reply-preview span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
2415
2435
  .reply-preview button { border: 0; background: transparent; color: inherit; cursor: pointer; }
2436
+ /* Said only when a message goes to people - the AI is who a message goes to otherwise, and that needs no label. */
2416
2437
  .send-destination {
2417
- font-size: 11px;
2438
+ display: flex;
2439
+ align-items: center;
2440
+ gap: 6px;
2441
+ margin: 2px 14px 0;
2442
+ font-size: 11.5px;
2418
2443
  color: var(--md-sys-color-on-surface-variant, #475569);
2419
- align-self: center;
2420
- max-width: 64px;
2444
+ }
2445
+ .send-destination md-icon {
2446
+ --md-icon-size: 15px;
2447
+ }
2448
+ /* A notice that must not move the conversation: it floats over the bottom of the messages. */
2449
+ .toast {
2450
+ position: absolute;
2451
+ left: 50%;
2452
+ bottom: 120px;
2453
+ transform: translateX(-50%);
2454
+ z-index: 5;
2455
+ padding: 4px 12px;
2456
+ border-radius: 999px;
2457
+ background: var(--md-sys-color-inverse-surface, #1e293b);
2458
+ color: var(--md-sys-color-inverse-on-surface, #f8fafc);
2459
+ font-size: 11.5px;
2460
+ white-space: nowrap;
2461
+ pointer-events: none;
2421
2462
  }
2422
2463
  .input-row {
2423
2464
  display: flex;
@@ -2494,42 +2535,57 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2494
2535
  --md-icon-size: 15px;
2495
2536
  }
2496
2537
 
2497
- /* One send control, aligned with the resting input height. */
2498
- .input-row > button {
2499
- width: 54px;
2500
- height: 54px;
2538
+ /*
2539
+ * Send and attach, two flat buttons stacked beside the box - together they are the box's resting height (54), so
2540
+ * the row's top and bottom lines meet. Without attachments the send button stands alone at the bottom.
2541
+ */
2542
+ .send-stack {
2543
+ display: flex;
2544
+ flex-direction: column;
2545
+ justify-content: flex-end;
2546
+ gap: 4px;
2547
+ flex-shrink: 0;
2548
+ align-self: flex-end;
2549
+ }
2550
+ .send-stack > button {
2551
+ width: 40px;
2552
+ height: 25px;
2501
2553
  padding: 0;
2502
- background: var(--md-sys-color-primary, #1e293b);
2503
- color: var(--md-sys-color-on-primary, #ffffff);
2504
- border: 0;
2505
- border-radius: 12px;
2554
+ border-radius: 6px;
2506
2555
  cursor: pointer;
2507
2556
  display: flex;
2508
2557
  align-items: center;
2509
2558
  justify-content: center;
2510
2559
  transition:
2511
2560
  background 0.15s,
2512
- transform 0.1s,
2513
2561
  opacity 0.15s;
2514
- flex-shrink: 0;
2515
- align-self: flex-end;
2516
- margin-bottom: 0;
2517
2562
  }
2518
- .input-row > button:disabled {
2563
+ .send-stack > .send {
2564
+ background: var(--md-sys-color-primary, #1e293b);
2565
+ color: var(--md-sys-color-on-primary, #ffffff);
2566
+ border: 0;
2567
+ }
2568
+ .send-stack > .attach {
2569
+ background: transparent;
2570
+ color: var(--md-sys-color-on-surface-variant, #475569);
2571
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2572
+ }
2573
+ .send-stack > .send:disabled {
2519
2574
  background: var(--md-sys-color-surface-container-highest, #e2e8f0);
2520
2575
  color: var(--md-sys-color-outline, #94a3b8);
2521
2576
  cursor: not-allowed;
2522
2577
  }
2523
- .input-row > button:hover:not(:disabled) {
2578
+ .send-stack > .send:hover:not(:disabled) {
2524
2579
  opacity: 0.88;
2525
2580
  }
2526
- .input-row > button:active:not(:disabled) {
2527
- transform: scale(0.94);
2581
+ .send-stack > .attach:hover {
2582
+ background: var(--md-sys-color-surface-container, #f8fafc);
2583
+ color: var(--md-sys-color-on-surface, #0f172a);
2528
2584
  }
2529
- .input-row > button md-icon {
2530
- --md-icon-size: 18px;
2585
+ .send-stack md-icon {
2586
+ --md-icon-size: 16px;
2531
2587
  }
2532
- .input-row > button .spinner {
2588
+ .send-stack .spinner {
2533
2589
  width: 12px;
2534
2590
  height: 12px;
2535
2591
  border: 1.8px solid currentColor;
@@ -2554,6 +2610,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2554
2610
  this.loadModeView();
2555
2611
  if (this.subject)
2556
2612
  this.onSubjectChanged();
2613
+ /* The domain's agents are read on every mount - a dock put back after closing sees an agent added meanwhile. */
2614
+ else
2615
+ void this.loadAgentChoices();
2557
2616
  /*
2558
2617
  * Put back after the dock was closed: what is being written stays, the conversation is read again and its
2559
2618
  * broadcast and presence start again - they were stopped when it was taken out.
@@ -2571,6 +2630,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2571
2630
  * 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
2572
2631
  */
2573
2632
  async onSubjectChanged() {
2633
+ void this.loadAgentChoices();
2574
2634
  await this.loadModeChoices();
2575
2635
  if (this.modeChoices.length < 2 || !this.subject || !this.mode)
2576
2636
  return;
@@ -2968,40 +3028,31 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2968
3028
  }
2969
3029
  }}
2970
3030
  />
2971
- ${this.acceptsAttachments
2972
- ? html `<button
2973
- type="button"
2974
- class="action"
2975
- @click=${() => this.renderRoot.querySelector('#chat-file-input')?.click()}
2976
- title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: 'attach an image (drag & drop or paste supported)' })}>
2977
- <md-icon>attach_file</md-icon>
2978
- ${i18next.t('ai-assistant.button.attach', { defaultValue: 'Attach' })}
2979
- </button>`
2980
- : nothing}
2981
3031
  ${hasMessages
2982
3032
  ? html `
2983
3033
  <button
2984
3034
  type="button"
2985
- class="action"
3035
+ class="quiet"
3036
+ aria-expanded=${this.examplesOpen ? 'true' : 'false'}
2986
3037
  @click=${() => (this.examplesOpen = !this.examplesOpen)}
2987
3038
  title=${i18next.t('ai-assistant.text.show-examples-tooltip')}>
2988
- <md-icon>${this.examplesOpen ? 'close' : 'lightbulb'}</md-icon>
2989
3039
  ${this.examplesOpen
2990
3040
  ? i18next.t('ai-assistant.button.close')
2991
3041
  : i18next.t('ai-assistant.button.examples')}
2992
3042
  </button>
2993
3043
  `
2994
3044
  : nothing}
3045
+ ${hasMessages ? html `<span class="quiet-sep" aria-hidden="true">·</span>` : nothing}
2995
3046
  <button
2996
3047
  type="button"
2997
- class="action composer-size"
3048
+ class="quiet composer-size"
2998
3049
  aria-pressed=${this.composerWide ? 'true' : 'false'}
2999
3050
  @click=${() => this.setComposerWide(!this.composerWide)}>
3000
- <md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
3001
3051
  ${this.composerWide
3002
3052
  ? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
3003
3053
  : i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
3004
3054
  </button>
3055
+ ${this.renderAgentPicker()}
3005
3056
  </div>
3006
3057
  ${this.renderTablePreview()}
3007
3058
  ${this.examplesOpen ? this.renderInlineExamples() : nothing}
@@ -3048,6 +3099,13 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3048
3099
  @click=${() => this.cancelReply()}><md-icon>close</md-icon></button>
3049
3100
  </div>` : nothing}
3050
3101
  ${this.renderInviteNotice()}
3102
+ ${this.addressesPeople
3103
+ ? html `<div class="send-destination" aria-live="polite">
3104
+ <md-icon>group</md-icon>${i18next.t('ai-assistant.text.send-to-people-notice', {
3105
+ defaultValue: 'This goes to the people in this conversation. The AI does not answer it.'
3106
+ })}
3107
+ </div>`
3108
+ : nothing}
3051
3109
  <div class="input-row">
3052
3110
  <div class="textarea-wrap">
3053
3111
  <textarea
@@ -3061,9 +3119,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3061
3119
  }}
3062
3120
  ?disabled=${this.busy}
3063
3121
  rows="1"
3064
- placeholder=${this.conversation.placeholder ??
3065
- this.placeholder ??
3066
- i18next.t('ai-assistant.text.input-placeholder')}></textarea>
3122
+ placeholder=${this.suggestion ?? this.conversation.placeholder ?? this.placeholder ?? i18next.t('ai-assistant.text.input-placeholder')}></textarea>
3067
3123
  ${this.mentionOpen
3068
3124
  ? html `
3069
3125
  <ox-mention-popup
@@ -3083,12 +3139,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3083
3139
  `
3084
3140
  : nothing}
3085
3141
  </div>
3086
- <span class="send-destination" aria-live="polite">${i18next.t(this.addressesPeople
3087
- ? 'ai-assistant.text.send-to-people' : 'ai-assistant.text.send-to-ai')}</span>
3088
3142
  ${this.composerWide
3089
3143
  ? html `<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
3090
3144
  : nothing}
3145
+ <div class="send-stack">
3091
3146
  <button
3147
+ class="send"
3092
3148
  ?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
3093
3149
  @click=${() => this.submitMessage()}
3094
3150
  aria-label=${i18next.t(this.addressesPeople ? 'ai-assistant.text.send-to-people' : 'ai-assistant.text.send-to-ai')}
@@ -3100,6 +3156,17 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3100
3156
  ? html `<span class="spinner"></span>`
3101
3157
  : html `<md-icon>arrow_upward</md-icon>`}
3102
3158
  </button>
3159
+ ${this.acceptsAttachments
3160
+ ? html `<button
3161
+ type="button"
3162
+ class="attach"
3163
+ aria-label=${i18next.t('ai-assistant.button.attach', { defaultValue: 'Attach' })}
3164
+ title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: 'attach an image (drag & drop or paste supported)' })}
3165
+ @click=${() => this.renderRoot.querySelector('#chat-file-input')?.click()}>
3166
+ <md-icon>attach_file</md-icon>
3167
+ </button>`
3168
+ : nothing}
3169
+ </div>
3103
3170
  </div>
3104
3171
  </div>
3105
3172
  `;
@@ -3325,8 +3392,14 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3325
3392
  return;
3326
3393
  }
3327
3394
  }
3328
- const action = composerKeyAction(e, this.composerWide);
3329
- if (action === 'narrow') {
3395
+ const action = composerKeyAction(e, this.composerWide, { empty: !this.input, suggestion: this.suggestion });
3396
+ if (action === 'take-suggestion') {
3397
+ e.preventDefault();
3398
+ this.input = this.suggestion;
3399
+ const ta = e.target;
3400
+ this.updateComplete.then(() => ta.setSelectionRange(ta.value.length, ta.value.length));
3401
+ }
3402
+ else if (action === 'narrow') {
3330
3403
  /* The Escape that narrows the box is spent here - the floating dock closes on the next one. */
3331
3404
  e.preventDefault();
3332
3405
  e.stopPropagation();
@@ -3338,6 +3411,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3338
3411
  void this.submitMessage();
3339
3412
  }
3340
3413
  }
3414
+ /** The request a Tab writes into the empty box - `composerSuggestion` decides it. */
3415
+ get suggestion() {
3416
+ return composerSuggestion({ started: this.lines.length > 0, modeExamples: this.modeView?.examples, hostExamples: this.examples });
3417
+ }
3341
3418
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
3342
3419
  * big board 에서 매 keystroke filter 비용 절감 + 한국어 IME 조합 중 깜빡임 완화. */
3343
3420
  onTextareaInput(e) {
@@ -3589,14 +3666,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3589
3666
  items: group.itemKeys.map(key => i18next.t(key))
3590
3667
  }));
3591
3668
  }
3592
- /** 이 표면이 실제로 쓸 수 있는 트리거 — 판정은 `chat-triggers.ts` 가 하고 여기서는 사실만 읽는다. */
3593
- get triggerChips() {
3594
- return availableTriggers({
3595
- hasMentionableThings: !!(this.boardProvider || this.currentBoard || this.knownTypes?.length),
3596
- hasSession: !!this.sessionId,
3597
- slashTemplateCount: this.conversation.slashTemplates.length
3598
- });
3599
- }
3600
3669
  static { this.EXAMPLE_GROUPS = [
3601
3670
  {
3602
3671
  labelKey: 'ai-assistant.label.create',
@@ -3647,10 +3716,32 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3647
3716
  renderModeSubject() {
3648
3717
  if (this.modeChoices.length < 2)
3649
3718
  return nothing;
3719
+ return html `<div class="mode-subject">${this.renderModePicker()}</div>`;
3720
+ }
3721
+ /**
3722
+ * Which AI answers the next turn - at the end of the row above the box, where the words about writing stand. A row of
3723
+ * its own spent a line of the dock on one control. It is a property of the turn, not of the conversation: switching
3724
+ * keeps the conversation and only the next answer comes from the other agent. Drawn only when there is a choice.
3725
+ */
3726
+ renderAgentPicker() {
3727
+ if (!offersAgentPicker(this.agentChoices))
3728
+ return nothing;
3729
+ const current = this.agentChoices.find(one => one.id === this.agentId);
3730
+ return html `
3731
+ <select
3732
+ class="mode-select agent-select"
3733
+ aria-label=${i18next.t('ai-assistant.label.ai-agent', { defaultValue: 'AI agent' })}
3734
+ title=${current ? agentDetail(current) : ''}
3735
+ ?disabled=${this.busy}
3736
+ @change=${(event) => this.onAgentPick(event.target.value)}>
3737
+ ${this.agentChoices.map(one => html `<option value=${one.id} ?selected=${one.id === this.agentId}>${one.name}</option>`)}
3738
+ </select>
3739
+ `;
3740
+ }
3741
+ renderModePicker() {
3650
3742
  const current = this.modeChoices.find(choice => choice.kind === this.mode?.kind);
3651
3743
  const others = this.modeChoices.filter(choice => choice.kind !== this.mode?.kind);
3652
3744
  return html `
3653
- <div class="mode-subject">
3654
3745
  <span class="mode-now">${current?.label ?? this.mode?.kind ?? ''}</span>
3655
3746
  ${others.length === 1
3656
3747
  ? html `
@@ -3672,7 +3763,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3672
3763
  ${this.modeChoices.map(choice => html `<option value=${choice.kind} ?selected=${choice.kind === this.mode?.kind}>${choice.label}</option>`)}
3673
3764
  </select>
3674
3765
  `}
3675
- </div>
3676
3766
  `;
3677
3767
  }
3678
3768
  renderSessionTabs() {
@@ -3731,25 +3821,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3731
3821
  </div>
3732
3822
  </div>
3733
3823
 
3734
- ${this.triggerChips.length > 0
3735
- ? html `
3736
- <div class="trigger-row">
3737
- <div class="trigger-row-label">${i18next.t('ai-assistant.text.triggers-heading')}</div>
3738
- <div class="trigger-chips">
3739
- ${this.triggerChips.map(trigger => html `
3740
- <button
3741
- class="trigger-chip"
3742
- title=${i18next.t(trigger.tooltipKey)}
3743
- @click=${() => this.insertTrigger(trigger.char)}>
3744
- <span class="trigger-char">${trigger.char}</span>
3745
- <span class="trigger-desc">${i18next.t(trigger.labelKey)}</span>
3746
- </button>
3747
- `)}
3748
- </div>
3749
- </div>
3750
- `
3751
- : ''}
3752
-
3753
3824
  ${this.exampleGroups.map(group => html `
3754
3825
  <div class="group">
3755
3826
  <div class="group-label">${group.label}</div>
@@ -3761,32 +3832,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3761
3832
  </div>
3762
3833
  `)}
3763
3834
 
3764
- <div class="footer">
3765
- <span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
3766
- <span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
3767
- <span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
3768
- ${this.conversation.footerNotice
3769
- ? html `<br />${this.conversation.footerNotice}`
3770
- : ''}
3771
- </div>
3835
+ ${this.conversation.footerNotice ? html `<div class="footer">${this.conversation.footerNotice}</div>` : ''}
3772
3836
  </div>
3773
3837
  `;
3774
3838
  }
3775
- /** trigger chip 클릭 — textarea 에 trigger 문자 삽입 + focus + popup 자동 활성. */
3776
- insertTrigger(trigger) {
3777
- // 입력 끝에 공백이 없으면 공백 추가 후 trigger — 자연스러운 위치
3778
- const needsSpace = this.input.length > 0 && !/\s$/.test(this.input);
3779
- this.input = this.input + (needsSpace ? ' ' : '') + trigger;
3780
- requestAnimationFrame(() => {
3781
- const ta = this.renderRoot.querySelector('textarea');
3782
- if (!ta)
3783
- return;
3784
- ta.focus();
3785
- ta.setSelectionRange(ta.value.length, ta.value.length);
3786
- // popup refresh — onTextareaInput 이 디바운스 후 호출되지만 직접 호출로 즉각 반영
3787
- this.refreshMentionPopup(ta);
3788
- });
3789
- }
3790
3839
  /** 예시 클릭 — 입력창에 채우고 포커스 (사용자가 수정 후 전송 가능) */
3791
3840
  useExample(text) {
3792
3841
  this.input = text;
@@ -4446,8 +4495,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4446
4495
  try {
4447
4496
  // 클립보드에는 marker 제거된 깨끗한 텍스트
4448
4497
  await navigator.clipboard.writeText(stripMentionRefids(content));
4498
+ /* The button turns into a check where it was pressed - that is the whole answer. */
4449
4499
  this.copiedIdx = idx;
4450
- this.showToast(i18next.t('ai-assistant.text.copied'));
4451
4500
  setTimeout(() => {
4452
4501
  if (this.copiedIdx === idx)
4453
4502
  this.copiedIdx = undefined;
@@ -4665,7 +4714,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4665
4714
  mode: { kind: this.conversation.kind, target: this.conversation.target },
4666
4715
  truncateAfterMessageId: this._truncateAfterId,
4667
4716
  hostContext: requestContext.hostContext,
4668
- attachmentIds
4717
+ attachmentIds,
4718
+ agentId: this.agentId
4669
4719
  })
4670
4720
  : buildChatMutationInput({
4671
4721
  sessionId: this.sessionId,
@@ -4684,7 +4734,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4684
4734
  selectedRefids: this.selectedRefids ?? [],
4685
4735
  mentions,
4686
4736
  /* With a declared mode the server owns persona and tools; the board door still carries the board. */
4687
- mode: this.conversation.kind ? { kind: this.conversation.kind, target: this.conversation.target } : undefined
4737
+ mode: this.conversation.kind ? { kind: this.conversation.kind, target: this.conversation.target } : undefined,
4738
+ agentId: this.agentId
4688
4739
  })
4689
4740
  }
4690
4741
  });
@@ -4996,6 +5047,14 @@ __decorate([
4996
5047
  state(),
4997
5048
  __metadata("design:type", Array)
4998
5049
  ], OxAssistantChat.prototype, "modeChoices", void 0);
5050
+ __decorate([
5051
+ state(),
5052
+ __metadata("design:type", Array)
5053
+ ], OxAssistantChat.prototype, "agentChoices", void 0);
5054
+ __decorate([
5055
+ state(),
5056
+ __metadata("design:type", String)
5057
+ ], OxAssistantChat.prototype, "agentId", void 0);
4999
5058
  OxAssistantChat = OxAssistantChat_1 = __decorate([
5000
5059
  customElement('ox-assistant-chat')
5001
5060
  ], OxAssistantChat);