@things-factory/ai-assistant 10.1.157 → 10.1.160

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 (75) hide show
  1. package/client/components/assistant-chat.ts +25 -213
  2. package/client/components/assistant-dock.ts +11 -32
  3. package/client/components/assistant-mode.test.ts +1 -43
  4. package/client/components/assistant-mode.ts +0 -47
  5. package/client/utils/assistant-surface.ts +1 -1
  6. package/client/utils/composer-frame.ts +6 -4
  7. package/dist-client/components/assistant-chat.d.ts +5 -69
  8. package/dist-client/components/assistant-chat.js +25 -205
  9. package/dist-client/components/assistant-chat.js.map +1 -1
  10. package/dist-client/components/assistant-dock.d.ts +0 -3
  11. package/dist-client/components/assistant-dock.js +10 -34
  12. package/dist-client/components/assistant-dock.js.map +1 -1
  13. package/dist-client/components/assistant-mode.d.ts +0 -33
  14. package/dist-client/components/assistant-mode.js +0 -37
  15. package/dist-client/components/assistant-mode.js.map +1 -1
  16. package/dist-client/components/assistant-mode.test.js +1 -35
  17. package/dist-client/components/assistant-mode.test.js.map +1 -1
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/dist-client/utils/assistant-surface.d.ts +1 -1
  20. package/dist-client/utils/assistant-surface.js.map +1 -1
  21. package/dist-client/utils/composer-frame.d.ts +5 -3
  22. package/dist-client/utils/composer-frame.js +4 -4
  23. package/dist-client/utils/composer-frame.js.map +1 -1
  24. package/dist-server/index.d.ts +1 -0
  25. package/dist-server/index.js +2 -0
  26. package/dist-server/index.js.map +1 -1
  27. package/dist-server/service/assistant-chat-resolver.d.ts +1 -0
  28. package/dist-server/service/assistant-chat-resolver.js +30 -6
  29. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  30. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +1 -2
  31. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +13 -24
  32. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  33. package/dist-server/service/assistant-mode/fallback-subjects.d.ts +1 -0
  34. package/dist-server/service/assistant-mode/fallback-subjects.js +37 -0
  35. package/dist-server/service/assistant-mode/fallback-subjects.js.map +1 -0
  36. package/dist-server/service/assistant-mode/index.d.ts +2 -1
  37. package/dist-server/service/assistant-mode/index.js +7 -2
  38. package/dist-server/service/assistant-mode/index.js.map +1 -1
  39. package/dist-server/service/assistant-mode/registry.d.ts +2 -6
  40. package/dist-server/service/assistant-mode/registry.js +16 -56
  41. package/dist-server/service/assistant-mode/registry.js.map +1 -1
  42. package/dist-server/service/assistant-mode/resolve.js +3 -1
  43. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  44. package/dist-server/service/assistant-mode/screen-rules.d.ts +25 -0
  45. package/dist-server/service/assistant-mode/screen-rules.js +47 -0
  46. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -0
  47. package/dist-server/service/assistant-mode/types.d.ts +8 -25
  48. package/dist-server/service/assistant-mode/types.js.map +1 -1
  49. package/dist-server/service/testing/mode-conformance.d.ts +11 -0
  50. package/dist-server/service/testing/mode-conformance.js +28 -0
  51. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  52. package/dist-server/tool-registry-boot-report.d.ts +1 -1
  53. package/dist-server/tool-registry-boot-report.js +16 -7
  54. package/dist-server/tool-registry-boot-report.js.map +1 -1
  55. package/dist-server/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +6 -6
  57. package/server/index.ts +2 -0
  58. package/server/service/assistant-chat-resolver.ts +33 -7
  59. package/server/service/assistant-mode/assistant-mode-resolver.ts +12 -25
  60. package/server/service/assistant-mode/fallback-subjects.ts +36 -0
  61. package/server/service/assistant-mode/index.ts +2 -1
  62. package/server/service/assistant-mode/one-conversation-per-subject.test.ts +37 -0
  63. package/server/service/assistant-mode/open-subject-door.test.ts +29 -0
  64. package/server/service/assistant-mode/registry.ts +14 -55
  65. package/server/service/assistant-mode/resolve.test.ts +10 -1
  66. package/server/service/assistant-mode/resolve.ts +3 -1
  67. package/server/service/assistant-mode/screen-rules.test.ts +39 -0
  68. package/server/service/assistant-mode/screen-rules.ts +47 -0
  69. package/server/service/assistant-mode/types.ts +8 -31
  70. package/server/service/testing/mode-conformance.ts +27 -1
  71. package/server/tool-registry-boot-report.test.ts +14 -1
  72. package/server/tool-registry-boot-report.ts +16 -7
  73. package/test/assistant-dock.test.ts +27 -4
  74. package/test/composer-frame.test.ts +12 -2
  75. package/server/service/assistant-mode/subject-defaults.test.ts +0 -86
@@ -136,13 +136,8 @@ export declare class OxAssistantChat extends LitElement {
136
136
  */
137
137
  mode?: AssistantMode;
138
138
  /**
139
- * 이 화면이 보여 주는 것의 **종류** — `'board'`, `'space'`, `'station'`, `'figure'` (ADR-0072 A17 ①).
140
- *
141
- * `mode` 가 「지금 어느 대화인가」라면 이것은 「그 자리에 어떤 대화들이 설 수 있나」이다. 서버가
142
- * `assistantModes(subject)` 로 답하고, 둘 이상이면 고르개가 뜬다. 하나면 고르개를 내지 않는다 —
143
- * 고를 것이 없는 고르개는 자리만 차지한다.
144
- *
145
- * 화면이 이것을 안 주면 고르개는 없고 host 가 정한 대화 하나만 산다. 그것이 오늘까지의 모양이다.
139
+ * 이 화면이 보여 주는 것의 **종류** — `'board'`, `'space'`, `'station'`, `'figure'`. 화면에 대화는 하나다
140
+ * (design/ai/screen-pattern.md §3). 바뀌면 사업장의 AI 에이전트 목록을 다시 읽는다.
146
141
  */
147
142
  subject?: string;
148
143
  /**
@@ -365,9 +360,6 @@ export declare class OxAssistantChat extends LitElement {
365
360
  */
366
361
  private modeView?;
367
362
  private modeViewToken;
368
- /** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
369
- private modeChoices;
370
- private modeChoicesToken;
371
363
  /** The AI agents this domain lets a person pick (design/ai/agent-settings.md §5). One or none draws no picker. */
372
364
  private agentChoices;
373
365
  private agentId?;
@@ -379,35 +371,6 @@ export declare class OxAssistantChat extends LitElement {
379
371
  */
380
372
  private loadAgentChoices;
381
373
  private onAgentPick;
382
- /**
383
- * 이 화면에 설 수 있는 대화를 서버에 묻는다.
384
- *
385
- * 화면이 종류 목록을 손에 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다. 목록은
386
- * 앱 서버의 선언에서 나온다(ADR-0072 A17 ①).
387
- */
388
- private loadModeChoices;
389
- /**
390
- * 사람이 다른 대화를 골랐다.
391
- *
392
- * 고른 것은 **이 subject 종류에 대해** 기억한다 — 보드를 열 때마다 저작 대화로 돌아가면, 현장을
393
- * 물으려던 사람은 화면을 열 때마다 같은 손질을 다시 해야 한다. 기억은 이 브라우저에만 남는다
394
- * (서버에 칸을 만들지 않는다 — A17 ①). 세션 자체는 자기가 어느 대화였는지 이미 안다.
395
- */
396
- private onModePick;
397
- /**
398
- * **고르는 것은 청하는 일이다** — `mode` 를 여기서 바꾸지 않는다.
399
- *
400
- * host 는 렌더마다 `mode` 를 다시 내려 준다. 패널이 자기 손으로 고쳐 두면 다음 렌더가 그것을 덮고,
401
- * 더 나쁜 것은 host 가 **자기가 무엇을 열었는지 모르는 채로 남는다**는 것이다. host 는 대화의 세션을
402
- * 대상별로 들고 있으므로, 대화가 바뀐 것을 모르면 앞 대화의 세션을 그대로 넘긴다 — 그러면 새 말이 앞
403
- * 대화에 붙는다(2026-09-18 Figure 레인이 :3300 에서 본 것이 이것이다).
404
- *
405
- * 그래서 주인은 하나다: host 가 `mode` 를 갖고, 패널은 「이것으로 바꿔 달라」고 말한다. 기억은 패널이
406
- * 진다(브라우저마다 다른 값이라 host 가 질 것이 아니다) — 기억한 것을 되살릴 때도 같은 청으로 나간다.
407
- */
408
- private askHostForMode;
409
- /** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
410
- private rememberedMode;
411
374
  private loadModeView;
412
375
  private cachedCatalogCandidates;
413
376
  private cachedSlashCandidates;
@@ -419,12 +382,7 @@ export declare class OxAssistantChat extends LitElement {
419
382
  private userFetchToken;
420
383
  static styles: import("lit").CSSResult[];
421
384
  connectedCallback(): void;
422
- /**
423
- * 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
424
- *
425
- * 되살리는 것은 **고른 대화**이지 세션이 아니다. 세션은 자기가 어느 대화였는지 이미 알고, 다른
426
- * 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
427
- */
385
+ /** 화면이 보여 주는 것의 종류가 바뀌었다 — 사업장의 AI 에이전트 목록을 다시 읽는다. */
428
386
  private onSubjectChanged;
429
387
  updated(changed: Map<string, any>): void;
430
388
  disconnectedCallback(): void;
@@ -478,6 +436,8 @@ export declare class OxAssistantChat extends LitElement {
478
436
  private onKeyDown;
479
437
  /** The request a Tab writes into the empty box - `composerSuggestion` decides it. */
480
438
  private get suggestion();
439
+ /** The next request the last answer named - gone once the person sends or the conversation changes. */
440
+ private nextRequest?;
481
441
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
482
442
  * big board 에서 매 keystroke filter 비용 절감 + 한국어 IME 조합 중 깜빡임 완화. */
483
443
  private onTextareaInput;
@@ -525,36 +485,12 @@ export declare class OxAssistantChat extends LitElement {
525
485
  private static readonly EXAMPLE_GROUPS;
526
486
  /** 보드의 세션 탭 — 다중 세션 활성화 시 패널 상단. host 가 sessions 비워두면 표시 X.
527
487
  * 레이블은 session.name 우선, 없으면 fallback "세션 N". */
528
- /**
529
- * 이 대화의 **주어** — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
530
- *
531
- * 하나뿐이면 아무것도 그리지 않는다. 고를 것이 없는 고르개는 자리만 차지하고, 사람에게 「무언가
532
- * 고를 것이 있다」는 잘못된 신호를 준다.
533
- *
534
- * ── 왜 탭 줄이 아닌가 (UX 레인, 2026-09-18) ───────────────────────────────
535
- * 처음에는 칩 줄에 `role="tablist"` 를 달았다. 바로 아래가 세션 탭이고 그쪽도 tablist 라서, 화면
536
- * 낭독기로 들으면 「탭 목록, 대화」 다음에 「탭 목록, Conversation history」 가 이어졌다 — 고르는
537
- * 축이 둘인데 둘 다 「탭」이라고 말한다. 게다가 ARIA 탭은 좌우 화살표 이동과 `tabpanel` 이 함께
538
- * 있어야 성립하고, 둘 다 없으면 낭독기는 「2 중 1번째 탭」이라 말하고 화살표는 아무 일도 안 한다.
539
- *
540
- * 무게도 다르다. 세션은 「어느 지난 대화냐」이고 모드는 「이 대화가 무엇에 대한 것이냐」이다. 같은
541
- * 무게로 그리면 어느 쪽이 화제를 바꾸고 어느 쪽이 실타래를 바꾸는지 구별되지 않는다. 그래서 지금
542
- * 대화가 제목 굵기로 서고, 바꿀 자리는 그 옆에 붙는다.
543
- *
544
- * ── 왜 도크 제목 줄로 안 올리나 (UX 레인 판정, 2026-09-18) ────────────────
545
- * 도크 제목은 「지금 어디에 있나」(이 도형 · 이 현장 · 이 스테이션 화면)이고 이 줄은 「이 대화가
546
- * 무엇에 대한 것이냐」이다. 대화의 주어는 대화 안에 있다. 그리고 세 도크가 각자 그리면 세 벌이 되고
547
- * 그 세 벌은 갈린다 — `modeChoices` 를 아는 것도 여기다. 올리면 목록과 고른 것이 세 자리로 나갔다
548
- * 돌아온다.
549
- */
550
- private renderModeSubject;
551
488
  /**
552
489
  * Which AI answers the next turn - at the end of the row above the box, where the words about writing stand. A row of
553
490
  * its own spent a line of the dock on one control. It is a property of the turn, not of the conversation: switching
554
491
  * keeps the conversation and only the next answer comes from the other agent. Drawn only when there is a choice.
555
492
  */
556
493
  private renderAgentPicker;
557
- private renderModePicker;
558
494
  private renderSessionTabs;
559
495
  /** C4 roster — 세션 참여자 + online 표시. 참여자 2명 미만이면 숨김(혼자면 의미 없음). */
560
496
  private renderRoster;
@@ -52,7 +52,7 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
52
52
  * 뜨지 않는 것이 맞다.
53
53
  */
54
54
  import { chatDefaults } from './chat-defaults.js';
55
- import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode.js';
55
+ import { isDifferentConversation, resolveConversation } from './assistant-mode.js';
56
56
  import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
57
57
  import './entry-review.js';
58
58
  import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
@@ -69,6 +69,7 @@ const BOARD_AI_CHAT_MUTATION = gql `
69
69
  mutation BoardAIChat($input: BoardAIChatInput!) {
70
70
  boardAIChat(input: $input) {
71
71
  reply
72
+ suggestion
72
73
  patch
73
74
  followUp
74
75
  clientId
@@ -107,6 +108,7 @@ const ASSISTANT_CHAT_MUTATION = gql `
107
108
  mutation AssistantChat($input: AssistantChatInput!) {
108
109
  assistantChat(input: $input) {
109
110
  reply
111
+ suggestion
110
112
  clientId
111
113
  sessionId
112
114
  userMessageId
@@ -126,20 +128,6 @@ const ASSISTANT_CHAT_MUTATION = gql `
126
128
  * (ADR-0061 decision 7) and translated there, so the panel and the door cannot describe different
127
129
  * conversations, and a host does not carry a second copy of the words.
128
130
  */
129
- /*
130
- * 이 화면에 설 수 있는 대화들 — ADR-0072 A17 ①.
131
- *
132
- * 화면은 자기가 무엇을 보여 주는지만 말하고, 그 위에 어느 대화가 서는지는 앱 서버가 답한다.
133
- */
134
- const ASSISTANT_MODES_QUERY = gql `
135
- query AssistantModes($subject: String!) {
136
- assistantModes(subject: $subject) {
137
- kind
138
- label
139
- isDefault
140
- }
141
- }
142
- `;
143
131
  const AI_AGENT_CHOICES_QUERY = gql `
144
132
  query AiAgentChoices {
145
133
  aiAgentChoices {
@@ -354,9 +342,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
354
342
  * send 시 mutation input.userMentions 로 함께 전송. */
355
343
  this.pickedUserMentions = new Map();
356
344
  this.modeViewToken = 0;
357
- /** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
358
- this.modeChoices = [];
359
- this.modeChoicesToken = 0;
360
345
  /** The AI agents this domain lets a person pick (design/ai/agent-settings.md §5). One or none draws no picker. */
361
346
  this.agentChoices = [];
362
347
  this.agentChoicesToken = 0;
@@ -745,81 +730,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
745
730
  this.agentId = agentId;
746
731
  rememberAgent(this.contextPath, agentId);
747
732
  }
748
- /**
749
- * 이 화면에 설 수 있는 대화를 서버에 묻는다.
750
- *
751
- * 화면이 종류 목록을 손에 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다. 목록은
752
- * 앱 서버의 선언에서 나온다(ADR-0072 A17 ①).
753
- */
754
- async loadModeChoices() {
755
- const subject = this.subject;
756
- if (!subject) {
757
- this.modeChoices = [];
758
- return;
759
- }
760
- const token = ++this.modeChoicesToken;
761
- try {
762
- const result = await client.query({
763
- query: ASSISTANT_MODES_QUERY,
764
- variables: { subject },
765
- fetchPolicy: 'cache-first'
766
- });
767
- if (token !== this.modeChoicesToken)
768
- return;
769
- this.modeChoices = result.data?.assistantModes ?? [];
770
- }
771
- catch {
772
- /*
773
- * 못 물어봤다고 화면을 막지 않는다 — host 가 준 대화는 그대로 산다. 고르개만 안 나온다.
774
- * 여기서 오류 문구를 띄우면 고를 것이 하나뿐인 화면에서도 빨간 줄이 뜬다.
775
- */
776
- if (token !== this.modeChoicesToken)
777
- return;
778
- this.modeChoices = [];
779
- }
780
- }
781
- /**
782
- * 사람이 다른 대화를 골랐다.
783
- *
784
- * 고른 것은 **이 subject 종류에 대해** 기억한다 — 보드를 열 때마다 저작 대화로 돌아가면, 현장을
785
- * 물으려던 사람은 화면을 열 때마다 같은 손질을 다시 해야 한다. 기억은 이 브라우저에만 남는다
786
- * (서버에 칸을 만들지 않는다 — A17 ①). 세션 자체는 자기가 어느 대화였는지 이미 안다.
787
- */
788
- onModePick(kind) {
789
- if (!this.mode || kind === this.mode.kind)
790
- return;
791
- try {
792
- if (this.subject)
793
- localStorage.setItem(`assistant-mode:${this.subject}`, kind);
794
- }
795
- catch {
796
- /* 저장이 막힌 브라우저에서도 고르기 자체는 된다 — 다음에 다시 고르면 될 뿐이다. */
797
- }
798
- this.askHostForMode(kind);
799
- }
800
- /**
801
- * **고르는 것은 청하는 일이다** — `mode` 를 여기서 바꾸지 않는다.
802
- *
803
- * host 는 렌더마다 `mode` 를 다시 내려 준다. 패널이 자기 손으로 고쳐 두면 다음 렌더가 그것을 덮고,
804
- * 더 나쁜 것은 host 가 **자기가 무엇을 열었는지 모르는 채로 남는다**는 것이다. host 는 대화의 세션을
805
- * 대상별로 들고 있으므로, 대화가 바뀐 것을 모르면 앞 대화의 세션을 그대로 넘긴다 — 그러면 새 말이 앞
806
- * 대화에 붙는다(2026-09-18 Figure 레인이 :3300 에서 본 것이 이것이다).
807
- *
808
- * 그래서 주인은 하나다: host 가 `mode` 를 갖고, 패널은 「이것으로 바꿔 달라」고 말한다. 기억은 패널이
809
- * 진다(브라우저마다 다른 값이라 host 가 질 것이 아니다) — 기억한 것을 되살릴 때도 같은 청으로 나간다.
810
- */
811
- askHostForMode(kind) {
812
- this.dispatchEvent(new CustomEvent('assistant-mode-change', { detail: { kind, subject: this.subject }, bubbles: true, composed: true }));
813
- }
814
- /** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
815
- rememberedMode(subject) {
816
- try {
817
- return localStorage.getItem(`assistant-mode:${subject}`) ?? undefined;
818
- }
819
- catch {
820
- return undefined;
821
- }
822
- }
823
733
  async loadModeView() {
824
734
  const mode = this.mode;
825
735
  if (!mode?.kind) {
@@ -877,31 +787,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
877
787
  background: var(--md-sys-color-surface, #ffffff);
878
788
  }
879
789
 
880
- /* ── C4 Roster (participants + presence) ───────────── */
881
- /* 이 대화의 주어 — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
882
- 지금 대화가 제목 굵기로 서고, 바꿀 자리가 그 옆에 붙는다. 세션 탭과 같은 무게로 그리지 않는다
883
- (UX 레인, 2026-09-18) — 한쪽은 화제를 바꾸고 한쪽은 실타래를 바꾼다. */
884
- .mode-subject {
885
- display: flex;
886
- align-items: baseline;
887
- gap: 8px;
888
- padding: 6px 12px;
889
- border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
890
- }
891
- .mode-now {
892
- /* 고른 상태의 색은 세션 탭과 같은 계열이다 — 한 패널 안에서 「골랐다」가 두 색으로 나오면 읽는
893
- 사람은 두 색이 서로 다른 뜻인 줄 안다.
894
-
895
- 12px 이었다가 한 단 내렸다. 도크 제목(figure 의 .scope-title 이 12.5px / 650)과 0.5px ·
896
- 굵기 50 차이는 눈에 닿지 않아서, 거의 같은 제목 줄이 둘 포개졌다. 도크 제목은 그 도크 전체를
897
- 거느리고 이 줄은 그 안 대화 하나의 성질이다 — 11px 이면 이 패널의 다른 조작부와 같은 단이고,
898
- 「이것이 주어다」는 색이 계속 진다(UX 레인, 2026-09-18). */
899
- color: var(--primary-color, #007f87);
900
- font-size: 11px;
901
- font-weight: 600;
902
- }
903
- .mode-switch,
904
- .mode-select {
790
+ /* The agent sits at the end of the row above the box. */
791
+ .agent-select {
905
792
  appearance: none;
906
793
  background: transparent;
907
794
  border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
@@ -912,24 +799,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
912
799
  font-size: 11px;
913
800
  padding: 2px 10px;
914
801
  white-space: nowrap;
915
- }
916
- /* The agent sits at the end of the row above the box. */
917
- .agent-select {
918
802
  margin-left: auto;
919
803
  max-width: 50%;
920
804
  text-overflow: ellipsis;
921
805
  }
922
- .mode-switch:hover:not([disabled]) {
923
- background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent);
924
- color: var(--primary-color, #007f87);
925
- }
926
- /* 답하는 동안에도 「내가 지금 무엇에 대해 말하고 있나」는 또렷해야 한다 — 흐려지는 것은 바꿀
927
- 자리뿐이다. 지금 대화까지 흐리면 그 한 줄이 가장 안 보인다. */
928
- .mode-subject [disabled] {
929
- cursor: default;
930
- opacity: 0.5;
931
- }
932
806
 
807
+ /* ── C4 Roster (participants + presence) ───────────── */
933
808
  .roster {
934
809
  display: flex;
935
810
  align-items: center;
@@ -2623,22 +2498,14 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2623
2498
  this._startMessageSubscription();
2624
2499
  }
2625
2500
  }
2626
- /**
2627
- * 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
2628
- *
2629
- * 되살리는 것은 **고른 대화**이지 세션이 아니다. 세션은 자기가 어느 대화였는지 이미 알고, 다른
2630
- * 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
2631
- */
2632
- async onSubjectChanged() {
2501
+ /** 화면이 보여 주는 것의 종류가 바뀌었다 — 사업장의 AI 에이전트 목록을 다시 읽는다. */
2502
+ onSubjectChanged() {
2633
2503
  void this.loadAgentChoices();
2634
- await this.loadModeChoices();
2635
- if (this.modeChoices.length < 2 || !this.subject || !this.mode)
2636
- return;
2637
- const kind = pickInitialMode(this.mode.kind, this.modeChoices, this.rememberedMode(this.subject));
2638
- if (kind && kind !== this.mode.kind)
2639
- this.askHostForMode(kind);
2640
2504
  }
2641
2505
  updated(changed) {
2506
+ /* Another conversation has no answer of this one to follow. */
2507
+ if (changed.has('sessionId') && changed.get('sessionId') !== undefined)
2508
+ this.nextRequest = undefined;
2642
2509
  if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments'))
2643
2510
  this.fitComposer();
2644
2511
  /*
@@ -2873,7 +2740,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2873
2740
  ${this.toastMessage
2874
2741
  ? html `<div class="toast show">${this.toastMessage}</div>`
2875
2742
  : nothing}
2876
- ${this.renderModeSubject()}
2877
2743
  ${this.renderSessionTabs()}
2878
2744
  ${this.renderRoster()}
2879
2745
  <div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
@@ -3413,7 +3279,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3413
3279
  }
3414
3280
  /** The request a Tab writes into the empty box - `composerSuggestion` decides it. */
3415
3281
  get suggestion() {
3416
- return composerSuggestion({ started: this.lines.length > 0, modeExamples: this.modeView?.examples, hostExamples: this.examples });
3282
+ return composerSuggestion({
3283
+ started: this.lines.length > 0,
3284
+ modeExamples: this.modeView?.examples,
3285
+ hostExamples: this.examples,
3286
+ answered: this.nextRequest
3287
+ });
3417
3288
  }
3418
3289
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
3419
3290
  * big board 에서 매 keystroke filter 비용 절감 + 한국어 IME 조합 중 깜빡임 완화. */
@@ -3691,33 +3562,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3691
3562
  ]; }
3692
3563
  /** 보드의 세션 탭 — 다중 세션 활성화 시 패널 상단. host 가 sessions 비워두면 표시 X.
3693
3564
  * 레이블은 session.name 우선, 없으면 fallback "세션 N". */
3694
- /**
3695
- * 이 대화의 **주어** — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
3696
- *
3697
- * 하나뿐이면 아무것도 그리지 않는다. 고를 것이 없는 고르개는 자리만 차지하고, 사람에게 「무언가
3698
- * 고를 것이 있다」는 잘못된 신호를 준다.
3699
- *
3700
- * ── 왜 탭 줄이 아닌가 (UX 레인, 2026-09-18) ───────────────────────────────
3701
- * 처음에는 칩 줄에 `role="tablist"` 를 달았다. 바로 아래가 세션 탭이고 그쪽도 tablist 라서, 화면
3702
- * 낭독기로 들으면 「탭 목록, 대화」 다음에 「탭 목록, Conversation history」 가 이어졌다 — 고르는
3703
- * 축이 둘인데 둘 다 「탭」이라고 말한다. 게다가 ARIA 탭은 좌우 화살표 이동과 `tabpanel` 이 함께
3704
- * 있어야 성립하고, 둘 다 없으면 낭독기는 「2 중 1번째 탭」이라 말하고 화살표는 아무 일도 안 한다.
3705
- *
3706
- * 무게도 다르다. 세션은 「어느 지난 대화냐」이고 모드는 「이 대화가 무엇에 대한 것이냐」이다. 같은
3707
- * 무게로 그리면 어느 쪽이 화제를 바꾸고 어느 쪽이 실타래를 바꾸는지 구별되지 않는다. 그래서 지금
3708
- * 대화가 제목 굵기로 서고, 바꿀 자리는 그 옆에 붙는다.
3709
- *
3710
- * ── 왜 도크 제목 줄로 안 올리나 (UX 레인 판정, 2026-09-18) ────────────────
3711
- * 도크 제목은 「지금 어디에 있나」(이 도형 · 이 현장 · 이 스테이션 화면)이고 이 줄은 「이 대화가
3712
- * 무엇에 대한 것이냐」이다. 대화의 주어는 대화 안에 있다. 그리고 세 도크가 각자 그리면 세 벌이 되고
3713
- * 그 세 벌은 갈린다 — `modeChoices` 를 아는 것도 여기다. 올리면 목록과 고른 것이 세 자리로 나갔다
3714
- * 돌아온다.
3715
- */
3716
- renderModeSubject() {
3717
- if (this.modeChoices.length < 2)
3718
- return nothing;
3719
- return html `<div class="mode-subject">${this.renderModePicker()}</div>`;
3720
- }
3721
3565
  /**
3722
3566
  * 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
3567
  * its own spent a line of the dock on one control. It is a property of the turn, not of the conversation: switching
@@ -3729,7 +3573,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3729
3573
  const current = this.agentChoices.find(one => one.id === this.agentId);
3730
3574
  return html `
3731
3575
  <select
3732
- class="mode-select agent-select"
3576
+ class="agent-select"
3733
3577
  aria-label=${i18next.t('ai-assistant.label.ai-agent', { defaultValue: 'AI agent' })}
3734
3578
  title=${current ? agentDetail(current) : ''}
3735
3579
  ?disabled=${this.busy}
@@ -3738,33 +3582,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3738
3582
  </select>
3739
3583
  `;
3740
3584
  }
3741
- renderModePicker() {
3742
- const current = this.modeChoices.find(choice => choice.kind === this.mode?.kind);
3743
- const others = this.modeChoices.filter(choice => choice.kind !== this.mode?.kind);
3744
- return html `
3745
- <span class="mode-now">${current?.label ?? this.mode?.kind ?? ''}</span>
3746
- ${others.length === 1
3747
- ? html `
3748
- <button
3749
- class="mode-switch"
3750
- title=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
3751
- aria-label=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
3752
- ?disabled=${this.busy}
3753
- @click=${() => this.onModePick(others[0].kind)}>
3754
- ${others[0].label}
3755
- </button>
3756
- `
3757
- : html `
3758
- <select
3759
- class="mode-select"
3760
- aria-label=${i18next.t('ai-assistant.label.conversation')}
3761
- ?disabled=${this.busy}
3762
- @change=${(event) => this.onModePick(event.target.value)}>
3763
- ${this.modeChoices.map(choice => html `<option value=${choice.kind} ?selected=${choice.kind === this.mode?.kind}>${choice.label}</option>`)}
3764
- </select>
3765
- `}
3766
- `;
3767
- }
3768
3585
  renderSessionTabs() {
3769
3586
  if (!this.sessions || this.sessions.length === 0)
3770
3587
  return nothing;
@@ -4572,6 +4389,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4572
4389
  const attachmentsToSend = [...this.attachments];
4573
4390
  if ((!text && attachmentsToSend.length === 0) || this.busy)
4574
4391
  return;
4392
+ this.nextRequest = undefined;
4575
4393
  this.attachments = [];
4576
4394
  /* What was written wide is sent to be answered - the conversation comes back to show it. */
4577
4395
  this.composerWide = false;
@@ -4752,6 +4570,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4752
4570
  throw new Error(i18next.t('ai-assistant.text.empty-response'));
4753
4571
  /* 대화가 길어졌다는 서버 보고 — 닫지 않았다면 안내를 띄운다. */
4754
4572
  this.historyFolded = out.historyFolded ?? undefined;
4573
+ /* The answer's next request - the input shows it and Tab writes it in (design/ai/screen-pattern.md §4-1). */
4574
+ this.nextRequest = typeof out.suggestion === 'string' && out.suggestion.trim() ? out.suggestion.trim() : undefined;
4755
4575
  // 전송 성공 — picks 정리 (다음 메시지부터는 새 picks 만)
4756
4576
  this.pickedMentions.clear();
4757
4577
  this.pickedUserMentions.clear();
@@ -5043,10 +4863,6 @@ __decorate([
5043
4863
  state(),
5044
4864
  __metadata("design:type", Object)
5045
4865
  ], OxAssistantChat.prototype, "modeView", void 0);
5046
- __decorate([
5047
- state(),
5048
- __metadata("design:type", Array)
5049
- ], OxAssistantChat.prototype, "modeChoices", void 0);
5050
4866
  __decorate([
5051
4867
  state(),
5052
4868
  __metadata("design:type", Array)
@@ -5055,6 +4871,10 @@ __decorate([
5055
4871
  state(),
5056
4872
  __metadata("design:type", String)
5057
4873
  ], OxAssistantChat.prototype, "agentId", void 0);
4874
+ __decorate([
4875
+ state(),
4876
+ __metadata("design:type", String)
4877
+ ], OxAssistantChat.prototype, "nextRequest", void 0);
5058
4878
  OxAssistantChat = OxAssistantChat_1 = __decorate([
5059
4879
  customElement('ox-assistant-chat')
5060
4880
  ], OxAssistantChat);