@things-factory/ai-assistant 10.1.156 → 10.1.159

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 +24 -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 +24 -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 +8 -8
  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';
@@ -107,6 +107,7 @@ const ASSISTANT_CHAT_MUTATION = gql `
107
107
  mutation AssistantChat($input: AssistantChatInput!) {
108
108
  assistantChat(input: $input) {
109
109
  reply
110
+ suggestion
110
111
  clientId
111
112
  sessionId
112
113
  userMessageId
@@ -126,20 +127,6 @@ const ASSISTANT_CHAT_MUTATION = gql `
126
127
  * (ADR-0061 decision 7) and translated there, so the panel and the door cannot describe different
127
128
  * conversations, and a host does not carry a second copy of the words.
128
129
  */
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
130
  const AI_AGENT_CHOICES_QUERY = gql `
144
131
  query AiAgentChoices {
145
132
  aiAgentChoices {
@@ -354,9 +341,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
354
341
  * send 시 mutation input.userMentions 로 함께 전송. */
355
342
  this.pickedUserMentions = new Map();
356
343
  this.modeViewToken = 0;
357
- /** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
358
- this.modeChoices = [];
359
- this.modeChoicesToken = 0;
360
344
  /** The AI agents this domain lets a person pick (design/ai/agent-settings.md §5). One or none draws no picker. */
361
345
  this.agentChoices = [];
362
346
  this.agentChoicesToken = 0;
@@ -745,81 +729,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
745
729
  this.agentId = agentId;
746
730
  rememberAgent(this.contextPath, agentId);
747
731
  }
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
732
  async loadModeView() {
824
733
  const mode = this.mode;
825
734
  if (!mode?.kind) {
@@ -877,31 +786,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
877
786
  background: var(--md-sys-color-surface, #ffffff);
878
787
  }
879
788
 
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 {
789
+ /* The agent sits at the end of the row above the box. */
790
+ .agent-select {
905
791
  appearance: none;
906
792
  background: transparent;
907
793
  border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
@@ -912,24 +798,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
912
798
  font-size: 11px;
913
799
  padding: 2px 10px;
914
800
  white-space: nowrap;
915
- }
916
- /* The agent sits at the end of the row above the box. */
917
- .agent-select {
918
801
  margin-left: auto;
919
802
  max-width: 50%;
920
803
  text-overflow: ellipsis;
921
804
  }
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
805
 
806
+ /* ── C4 Roster (participants + presence) ───────────── */
933
807
  .roster {
934
808
  display: flex;
935
809
  align-items: center;
@@ -2623,22 +2497,14 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2623
2497
  this._startMessageSubscription();
2624
2498
  }
2625
2499
  }
2626
- /**
2627
- * 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
2628
- *
2629
- * 되살리는 것은 **고른 대화**이지 세션이 아니다. 세션은 자기가 어느 대화였는지 이미 알고, 다른
2630
- * 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
2631
- */
2632
- async onSubjectChanged() {
2500
+ /** 화면이 보여 주는 것의 종류가 바뀌었다 — 사업장의 AI 에이전트 목록을 다시 읽는다. */
2501
+ onSubjectChanged() {
2633
2502
  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
2503
  }
2641
2504
  updated(changed) {
2505
+ /* Another conversation has no answer of this one to follow. */
2506
+ if (changed.has('sessionId') && changed.get('sessionId') !== undefined)
2507
+ this.nextRequest = undefined;
2642
2508
  if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments'))
2643
2509
  this.fitComposer();
2644
2510
  /*
@@ -2873,7 +2739,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2873
2739
  ${this.toastMessage
2874
2740
  ? html `<div class="toast show">${this.toastMessage}</div>`
2875
2741
  : nothing}
2876
- ${this.renderModeSubject()}
2877
2742
  ${this.renderSessionTabs()}
2878
2743
  ${this.renderRoster()}
2879
2744
  <div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
@@ -3413,7 +3278,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3413
3278
  }
3414
3279
  /** The request a Tab writes into the empty box - `composerSuggestion` decides it. */
3415
3280
  get suggestion() {
3416
- return composerSuggestion({ started: this.lines.length > 0, modeExamples: this.modeView?.examples, hostExamples: this.examples });
3281
+ return composerSuggestion({
3282
+ started: this.lines.length > 0,
3283
+ modeExamples: this.modeView?.examples,
3284
+ hostExamples: this.examples,
3285
+ answered: this.nextRequest
3286
+ });
3417
3287
  }
3418
3288
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
3419
3289
  * big board 에서 매 keystroke filter 비용 절감 + 한국어 IME 조합 중 깜빡임 완화. */
@@ -3691,33 +3561,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3691
3561
  ]; }
3692
3562
  /** 보드의 세션 탭 — 다중 세션 활성화 시 패널 상단. host 가 sessions 비워두면 표시 X.
3693
3563
  * 레이블은 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
3564
  /**
3722
3565
  * 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
3566
  * 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 +3572,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3729
3572
  const current = this.agentChoices.find(one => one.id === this.agentId);
3730
3573
  return html `
3731
3574
  <select
3732
- class="mode-select agent-select"
3575
+ class="agent-select"
3733
3576
  aria-label=${i18next.t('ai-assistant.label.ai-agent', { defaultValue: 'AI agent' })}
3734
3577
  title=${current ? agentDetail(current) : ''}
3735
3578
  ?disabled=${this.busy}
@@ -3738,33 +3581,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3738
3581
  </select>
3739
3582
  `;
3740
3583
  }
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
3584
  renderSessionTabs() {
3769
3585
  if (!this.sessions || this.sessions.length === 0)
3770
3586
  return nothing;
@@ -4572,6 +4388,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4572
4388
  const attachmentsToSend = [...this.attachments];
4573
4389
  if ((!text && attachmentsToSend.length === 0) || this.busy)
4574
4390
  return;
4391
+ this.nextRequest = undefined;
4575
4392
  this.attachments = [];
4576
4393
  /* What was written wide is sent to be answered - the conversation comes back to show it. */
4577
4394
  this.composerWide = false;
@@ -4752,6 +4569,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4752
4569
  throw new Error(i18next.t('ai-assistant.text.empty-response'));
4753
4570
  /* 대화가 길어졌다는 서버 보고 — 닫지 않았다면 안내를 띄운다. */
4754
4571
  this.historyFolded = out.historyFolded ?? undefined;
4572
+ /* The answer's next request - the input shows it and Tab writes it in (design/ai/screen-pattern.md §4-1). */
4573
+ this.nextRequest = typeof out.suggestion === 'string' && out.suggestion.trim() ? out.suggestion.trim() : undefined;
4755
4574
  // 전송 성공 — picks 정리 (다음 메시지부터는 새 picks 만)
4756
4575
  this.pickedMentions.clear();
4757
4576
  this.pickedUserMentions.clear();
@@ -5043,10 +4862,6 @@ __decorate([
5043
4862
  state(),
5044
4863
  __metadata("design:type", Object)
5045
4864
  ], OxAssistantChat.prototype, "modeView", void 0);
5046
- __decorate([
5047
- state(),
5048
- __metadata("design:type", Array)
5049
- ], OxAssistantChat.prototype, "modeChoices", void 0);
5050
4865
  __decorate([
5051
4866
  state(),
5052
4867
  __metadata("design:type", Array)
@@ -5055,6 +4870,10 @@ __decorate([
5055
4870
  state(),
5056
4871
  __metadata("design:type", String)
5057
4872
  ], OxAssistantChat.prototype, "agentId", void 0);
4873
+ __decorate([
4874
+ state(),
4875
+ __metadata("design:type", String)
4876
+ ], OxAssistantChat.prototype, "nextRequest", void 0);
5058
4877
  OxAssistantChat = OxAssistantChat_1 = __decorate([
5059
4878
  customElement('ox-assistant-chat')
5060
4879
  ], OxAssistantChat);