@things-factory/board-ai 10.0.0 → 10.0.2

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 (67) hide show
  1. package/client/components/board-ai-chat.ts +107 -2
  2. package/client/components/chat-echo-dedup.ts +3 -0
  3. package/client/components/chat-input-builder.ts +6 -0
  4. package/dist-client/client/components/board-ai-chat.d.ts +17 -0
  5. package/dist-client/client/components/board-ai-chat.js +102 -2
  6. package/dist-client/client/components/board-ai-chat.js.map +1 -1
  7. package/dist-client/client/components/chat-echo-dedup.d.ts +2 -0
  8. package/dist-client/client/components/chat-echo-dedup.js +1 -0
  9. package/dist-client/client/components/chat-echo-dedup.js.map +1 -1
  10. package/dist-client/client/components/chat-input-builder.d.ts +5 -0
  11. package/dist-client/client/components/chat-input-builder.js +1 -0
  12. package/dist-client/client/components/chat-input-builder.js.map +1 -1
  13. package/dist-client/server/service/agentic-loop.d.ts +18 -0
  14. package/dist-client/server/service/agentic-loop.js +10 -1
  15. package/dist-client/server/service/agentic-loop.js.map +1 -1
  16. package/dist-client/server/service/assistant.js +14 -2
  17. package/dist-client/server/service/assistant.js.map +1 -1
  18. package/dist-client/server/service/grounding.d.ts +17 -0
  19. package/dist-client/server/service/grounding.js +42 -0
  20. package/dist-client/server/service/grounding.js.map +1 -0
  21. package/dist-client/server/service/types.d.ts +16 -0
  22. package/dist-client/server/service/types.js.map +1 -1
  23. package/dist-client/tsconfig.tsbuildinfo +1 -1
  24. package/dist-server/service/agentic-loop.d.ts +18 -0
  25. package/dist-server/service/agentic-loop.js +10 -1
  26. package/dist-server/service/agentic-loop.js.map +1 -1
  27. package/dist-server/service/assistant.js +14 -2
  28. package/dist-server/service/assistant.js.map +1 -1
  29. package/dist-server/service/board-ai-resolver.d.ts +2 -0
  30. package/dist-server/service/board-ai-resolver.js +22 -1
  31. package/dist-server/service/board-ai-resolver.js.map +1 -1
  32. package/dist-server/service/chat-message/chat-message.d.ts +12 -0
  33. package/dist-server/service/chat-message/chat-message.js +23 -0
  34. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  35. package/dist-server/service/chat-session/session-inbox.d.ts +26 -0
  36. package/dist-server/service/chat-session/session-inbox.js +41 -0
  37. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  38. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +11 -0
  39. package/dist-server/service/chat-session-participant/chat-session-participant.js +17 -1
  40. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -1
  41. package/dist-server/service/chat-session-resolver.d.ts +44 -1
  42. package/dist-server/service/chat-session-resolver.js +306 -6
  43. package/dist-server/service/chat-session-resolver.js.map +1 -1
  44. package/dist-server/service/grounding.d.ts +17 -0
  45. package/dist-server/service/grounding.js +46 -0
  46. package/dist-server/service/grounding.js.map +1 -0
  47. package/dist-server/service/types.d.ts +16 -0
  48. package/dist-server/service/types.js.map +1 -1
  49. package/dist-server/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +5 -5
  51. package/server/service/agentic-loop.test.ts +63 -0
  52. package/server/service/agentic-loop.ts +28 -1
  53. package/server/service/assistant.ts +15 -2
  54. package/server/service/board-ai-resolver.ts +23 -1
  55. package/server/service/chat-message/chat-message.ts +26 -0
  56. package/server/service/chat-session/session-inbox.test.ts +69 -1
  57. package/server/service/chat-session/session-inbox.ts +45 -0
  58. package/server/service/chat-session-participant/chat-session-participant.ts +14 -0
  59. package/server/service/chat-session-resolver.ts +297 -5
  60. package/server/service/grounding.test.ts +55 -0
  61. package/server/service/grounding.ts +53 -0
  62. package/server/service/types.ts +16 -0
  63. package/translations/en.json +3 -1
  64. package/translations/ja.json +3 -1
  65. package/translations/ko.json +2 -0
  66. package/translations/ms.json +3 -1
  67. package/translations/zh.json +3 -1
@@ -70,6 +70,13 @@ interface ChatLine {
70
70
  toolUsages?: Array<{ name: string; arguments: any; result: any; kind: 'read' | 'write' | 'unknown' }>
71
71
  /** UI 상태: tool usage 박스 펼침 여부 (assistant 메시지마다 독립). */
72
72
  toolUsagesOpen?: boolean
73
+ /** UI 상태: 접힌 시스템 기록을 펼쳤는가(곁줄이 대화를 가리지 않게 기본은 3줄 접기). */
74
+ systemOpen?: boolean
75
+ /**
76
+ * 접지 경고 — 이 답이 언급했으나 모델이 받은 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자.
77
+ * 있으면 그 대상은 만들어 낸 것일 수 있다는 주의를 답 아래에 표시한다(답 자체는 그대로 보여준다).
78
+ */
79
+ groundingWarnings?: string[]
73
80
  }
74
81
 
75
82
  const BOARD_AI_CHAT_MUTATION = gql`
@@ -85,6 +92,7 @@ const BOARD_AI_CHAT_MUTATION = gql`
85
92
  patchId
86
93
  toolUsages
87
94
  actions
95
+ groundingWarnings
88
96
  }
89
97
  }
90
98
  `
@@ -97,6 +105,7 @@ const CHAT_MESSAGES_QUERY = gql`
97
105
  content
98
106
  relatedPatchId
99
107
  toolUsagesJson
108
+ groundingWarningsJson
100
109
  createdAt
101
110
  creator {
102
111
  name
@@ -116,6 +125,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql`
116
125
  content
117
126
  relatedPatchId
118
127
  toolUsagesJson
128
+ groundingWarningsJson
119
129
  createdAt
120
130
  creator {
121
131
  name
@@ -253,6 +263,15 @@ export class OxBoardAIChat extends LitElement {
253
263
  @property({ type: Boolean, attribute: 'board-tools' })
254
264
  boardTools = true
255
265
 
266
+ /**
267
+ * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
268
+ *
269
+ * 접지 가드는 지어낸 식별자를 잡지만 식별자 없는 상황 서술("바쁘게 움직이고 있다")은 잡지 못한다.
270
+ * 그건 도구를 부르지 않고 답한 경우이므로, 조회를 먼저 하게 만들어 원천을 막는다.
271
+ */
272
+ @property({ type: Boolean, attribute: 'require-grounding-tools' })
273
+ requireGroundingTools = false
274
+
256
275
  /** board-import registry scopes. */
257
276
  @property({ type: Array })
258
277
  scopes?: string[]
@@ -951,6 +970,21 @@ export class OxBoardAIChat extends LitElement {
951
970
  margin: 0;
952
971
  border: 0;
953
972
  }
973
+ /* 시스템 기록은 **곁줄**이다 — 길면 화면을 잡아먹어 대화가 안 보인다(옮겨온 대화를 한 덩어리로
974
+ 넣었을 때 실제로 그랬다). 세 줄로 접고 눌러서 펼친다. */
975
+ .msg.system .sys-text {
976
+ display: -webkit-box;
977
+ -webkit-box-orient: vertical;
978
+ -webkit-line-clamp: 3;
979
+ overflow: hidden;
980
+ white-space: pre-wrap;
981
+ cursor: pointer;
982
+ }
983
+ .msg.system.open .sys-text {
984
+ display: block;
985
+ -webkit-line-clamp: unset;
986
+ overflow: visible;
987
+ }
954
988
 
955
989
  .msg.pending {
956
990
  opacity: 0.5;
@@ -1039,6 +1073,30 @@ export class OxBoardAIChat extends LitElement {
1039
1073
  line-height: 1.45;
1040
1074
  }
1041
1075
 
1076
+ /* ── 접지 경고 — 근거에 없는 대상을 지목한 답 ───── */
1077
+ .grounding-warning {
1078
+ display: flex;
1079
+ align-items: flex-start;
1080
+ gap: 6px;
1081
+ margin-top: 4px;
1082
+ padding: 5px 8px;
1083
+ border-radius: 6px;
1084
+ border: 1px solid var(--md-sys-color-error, #dc2626);
1085
+ background: var(--md-sys-color-error-container, #fef2f2);
1086
+ color: var(--md-sys-color-on-error-container, #7f1d1d);
1087
+ font-size: 10.5px;
1088
+ line-height: 1.45;
1089
+ }
1090
+ .grounding-warning md-icon {
1091
+ --md-icon-size: 14px;
1092
+ flex-shrink: 0;
1093
+ }
1094
+ .grounding-warning .ids {
1095
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1096
+ font-weight: 600;
1097
+ word-break: break-all;
1098
+ }
1099
+
1042
1100
  /* ── Tool usages fold-able 박스 — AI 도구 호출 trace ───── */
1043
1101
  .tool-usages {
1044
1102
  margin-top: 4px;
@@ -1645,12 +1703,22 @@ export class OxBoardAIChat extends LitElement {
1645
1703
  ? html`<div class="msg-label sender">${line.senderName || i18next.t('board-ai.label.participant')}</div>`
1646
1704
  : nothing}
1647
1705
  <div class="msg-wrap ${line.role} ${isOther ? 'other' : ''}">
1648
- <div class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''}">
1706
+ <div
1707
+ class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''} ${line.systemOpen
1708
+ ? 'open'
1709
+ : ''}">
1649
1710
  ${line.pending
1650
1711
  ? html`<span class="typing-dots"><span></span><span></span><span></span></span>`
1651
1712
  : isMarkdown
1652
1713
  ? html`<div class="md-body">${unsafeHTML(renderMarkdown(content))}</div>`
1653
- : content}
1714
+ : line.role === 'system'
1715
+ ? html`<span
1716
+ class="sys-text"
1717
+ title=${i18next.t('board-ai.text.toggle-system-note', { defaultValue: '눌러서 펼치기' })}
1718
+ @click=${() => this.toggleSystemNote(idx)}
1719
+ >${content}</span
1720
+ >`
1721
+ : content}
1654
1722
  ${line.patchId
1655
1723
  ? html`
1656
1724
  <div class="summary ${this.revertedPatchIds.has(line.patchId) ? 'reverted' : ''}">
@@ -1672,6 +1740,9 @@ export class OxBoardAIChat extends LitElement {
1672
1740
  ${line.followUp
1673
1741
  ? html`<div class="followup">${unsafeHTML(renderMarkdown(line.followUp.trim()))}</div>`
1674
1742
  : nothing}
1743
+ ${line.groundingWarnings && line.groundingWarnings.length > 0
1744
+ ? this.renderGroundingWarning(line)
1745
+ : nothing}
1675
1746
  ${line.toolUsages && line.toolUsages.length > 0
1676
1747
  ? this.renderToolUsages(idx, line)
1677
1748
  : nothing}
@@ -2327,6 +2398,29 @@ export class OxBoardAIChat extends LitElement {
2327
2398
  `
2328
2399
  }
2329
2400
 
2401
+ /**
2402
+ * 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
2403
+ *
2404
+ * 왜 답을 지우지 않는가: 판정은 식별자 대조라 확정이 아니다(예: 사용자가 화면에서 본 이름을
2405
+ * 대화 밖에서 알고 말한 경우). 그래서 답은 그대로 보여주고, 검증되지 않았다는 사실만 붙인다 —
2406
+ * 사용자가 그 대상을 실제 작업의 근거로 삼기 전에 알아야 하는 정보다.
2407
+ */
2408
+ private renderGroundingWarning(line: ChatLine) {
2409
+ const ids = line.groundingWarnings ?? []
2410
+ if (ids.length === 0) return nothing
2411
+ return html`
2412
+ <div class="grounding-warning" role="note">
2413
+ <md-icon>report</md-icon>
2414
+ <span>
2415
+ ${i18next.t('board-ai.text.ungrounded-mention', {
2416
+ defaultValue: '확인되지 않은 대상을 언급했습니다 — 실제 데이터에 없습니다:'
2417
+ })}
2418
+ <span class="ids">${ids.join(', ')}</span>
2419
+ </span>
2420
+ </div>
2421
+ `
2422
+ }
2423
+
2330
2424
  /**
2331
2425
  * "AI 가 이런 도구를 사용했습니다" fold-able 박스.
2332
2426
  *
@@ -2379,6 +2473,15 @@ export class OxBoardAIChat extends LitElement {
2379
2473
  `
2380
2474
  }
2381
2475
 
2476
+ /** 시스템 기록 펼치기/접기 — 곁줄이 길어도 대화가 가려지지 않게. */
2477
+ private toggleSystemNote(idx: number) {
2478
+ const next = [...this.lines]
2479
+ const line = next[idx]
2480
+ if (!line) return
2481
+ next[idx] = { ...line, systemOpen: !line.systemOpen }
2482
+ this.lines = next
2483
+ }
2484
+
2382
2485
  private toggleToolUsages(idx: number) {
2383
2486
  const next = [...this.lines]
2384
2487
  const line = next[idx]
@@ -2534,6 +2637,7 @@ export class OxBoardAIChat extends LitElement {
2534
2637
  hostContext: this.hostContext,
2535
2638
  toolCategories: this.toolCategories,
2536
2639
  boardTools: this.boardTools,
2640
+ requireGroundingTools: this.requireGroundingTools,
2537
2641
  knownTypes: this.knownTypes,
2538
2642
  categories: this.categories,
2539
2643
  componentSchemas: this.componentSchemas,
@@ -2560,6 +2664,7 @@ export class OxBoardAIChat extends LitElement {
2560
2664
  followUp: out.followUp || undefined,
2561
2665
  patchId: out.patchId || undefined,
2562
2666
  toolUsages: Array.isArray(out.toolUsages) ? out.toolUsages : undefined,
2667
+ groundingWarnings: Array.isArray(out.groundingWarnings) ? out.groundingWarnings : undefined,
2563
2668
  pending: false
2564
2669
  })
2565
2670
 
@@ -27,6 +27,8 @@ export interface RemoteMessage {
27
27
  content: string
28
28
  relatedPatchId?: string
29
29
  toolUsagesJson?: any
30
+ /** 접지 경고(영속) — 답이 언급했으나 근거에 없던 식별자. 나중에 읽는 사람도 같은 주의를 봐야 한다. */
31
+ groundingWarningsJson?: any
30
32
  /** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */
31
33
  creator?: { name?: string; email?: string } | null
32
34
  }
@@ -39,6 +41,7 @@ export function remoteMessageToLine(m: RemoteMessage): DedupLine {
39
41
  content: m.content,
40
42
  patchId: m.relatedPatchId ?? undefined,
41
43
  toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,
44
+ groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,
42
45
  senderEmail: m.creator?.email ?? undefined,
43
46
  senderName: m.creator?.name ?? undefined
44
47
  }
@@ -21,6 +21,11 @@ export interface ChatMutationInputArgs {
21
21
  toolCategories?: string[]
22
22
  /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */
23
23
  boardTools?: boolean
24
+ /**
25
+ * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.
26
+ * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.
27
+ */
28
+ requireGroundingTools?: boolean
24
29
  knownTypes?: string[]
25
30
  categories?: string[]
26
31
  componentSchemas?: any
@@ -54,6 +59,7 @@ export function buildChatMutationInput(args: ChatMutationInputArgs): Record<stri
54
59
  /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */
55
60
  ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),
56
61
  ...(args.boardTools === false ? { boardTools: false } : {}),
62
+ ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),
57
63
  knownTypes: args.knownTypes,
58
64
  categories: args.categories,
59
65
  componentSchemas: args.componentSchemas,
@@ -100,6 +100,13 @@ export declare class OxBoardAIChat extends LitElement {
100
100
  hostContext?: any;
101
101
  /** 코어 보드 편집 도구 사용 여부. 기본 true. 보드를 고칠 이유가 없는 면에서 false. */
102
102
  boardTools: boolean;
103
+ /**
104
+ * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
105
+ *
106
+ * 접지 가드는 지어낸 식별자를 잡지만 식별자 없는 상황 서술("바쁘게 움직이고 있다")은 잡지 못한다.
107
+ * 그건 도구를 부르지 않고 답한 경우이므로, 조회를 먼저 하게 만들어 원천을 막는다.
108
+ */
109
+ requireGroundingTools: boolean;
103
110
  /** board-import registry scopes. */
104
111
  scopes?: string[];
105
112
  knownTypes?: string[];
@@ -261,6 +268,14 @@ export declare class OxBoardAIChat extends LitElement {
261
268
  * system : 없음
262
269
  */
263
270
  private renderMessageActions;
271
+ /**
272
+ * 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
273
+ *
274
+ * 왜 답을 지우지 않는가: 판정은 식별자 대조라 확정이 아니다(예: 사용자가 화면에서 본 이름을
275
+ * 대화 밖에서 알고 말한 경우). 그래서 답은 그대로 보여주고, 검증되지 않았다는 사실만 붙인다 —
276
+ * 사용자가 그 대상을 실제 작업의 근거로 삼기 전에 알아야 하는 정보다.
277
+ */
278
+ private renderGroundingWarning;
264
279
  /**
265
280
  * "AI 가 이런 도구를 사용했습니다" fold-able 박스.
266
281
  *
@@ -269,6 +284,8 @@ export declare class OxBoardAIChat extends LitElement {
269
284
  * - 디버그 / 신뢰도 향상용 — 사용자가 "왜 이렇게 답했지" 의문 가질 때 즉시 검증.
270
285
  */
271
286
  private renderToolUsages;
287
+ /** 시스템 기록 펼치기/접기 — 곁줄이 길어도 대화가 가려지지 않게. */
288
+ private toggleSystemNote;
272
289
  private toggleToolUsages;
273
290
  private formatJson;
274
291
  private copyMessage;
@@ -44,6 +44,7 @@ const BOARD_AI_CHAT_MUTATION = gql `
44
44
  patchId
45
45
  toolUsages
46
46
  actions
47
+ groundingWarnings
47
48
  }
48
49
  }
49
50
  `;
@@ -55,6 +56,7 @@ const CHAT_MESSAGES_QUERY = gql `
55
56
  content
56
57
  relatedPatchId
57
58
  toolUsagesJson
59
+ groundingWarningsJson
58
60
  createdAt
59
61
  creator {
60
62
  name
@@ -73,6 +75,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql `
73
75
  content
74
76
  relatedPatchId
75
77
  toolUsagesJson
78
+ groundingWarningsJson
76
79
  createdAt
77
80
  creator {
78
81
  name
@@ -136,6 +139,13 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
136
139
  this.selectedRefids = [];
137
140
  /** 코어 보드 편집 도구 사용 여부. 기본 true. 보드를 고칠 이유가 없는 면에서 false. */
138
141
  this.boardTools = true;
142
+ /**
143
+ * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
144
+ *
145
+ * 접지 가드는 지어낸 식별자를 잡지만 식별자 없는 상황 서술("바쁘게 움직이고 있다")은 잡지 못한다.
146
+ * 그건 도구를 부르지 않고 답한 경우이므로, 조회를 먼저 하게 만들어 원천을 막는다.
147
+ */
148
+ this.requireGroundingTools = false;
139
149
  this.placeholder = '자연어로 명령하세요 (Shift+Enter 줄바꿈, Enter 전송)';
140
150
  this.lines = [];
141
151
  this.input = '';
@@ -789,6 +799,21 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
789
799
  margin: 0;
790
800
  border: 0;
791
801
  }
802
+ /* 시스템 기록은 **곁줄**이다 — 길면 화면을 잡아먹어 대화가 안 보인다(옮겨온 대화를 한 덩어리로
803
+ 넣었을 때 실제로 그랬다). 세 줄로 접고 눌러서 펼친다. */
804
+ .msg.system .sys-text {
805
+ display: -webkit-box;
806
+ -webkit-box-orient: vertical;
807
+ -webkit-line-clamp: 3;
808
+ overflow: hidden;
809
+ white-space: pre-wrap;
810
+ cursor: pointer;
811
+ }
812
+ .msg.system.open .sys-text {
813
+ display: block;
814
+ -webkit-line-clamp: unset;
815
+ overflow: visible;
816
+ }
792
817
 
793
818
  .msg.pending {
794
819
  opacity: 0.5;
@@ -877,6 +902,30 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
877
902
  line-height: 1.45;
878
903
  }
879
904
 
905
+ /* ── 접지 경고 — 근거에 없는 대상을 지목한 답 ───── */
906
+ .grounding-warning {
907
+ display: flex;
908
+ align-items: flex-start;
909
+ gap: 6px;
910
+ margin-top: 4px;
911
+ padding: 5px 8px;
912
+ border-radius: 6px;
913
+ border: 1px solid var(--md-sys-color-error, #dc2626);
914
+ background: var(--md-sys-color-error-container, #fef2f2);
915
+ color: var(--md-sys-color-on-error-container, #7f1d1d);
916
+ font-size: 10.5px;
917
+ line-height: 1.45;
918
+ }
919
+ .grounding-warning md-icon {
920
+ --md-icon-size: 14px;
921
+ flex-shrink: 0;
922
+ }
923
+ .grounding-warning .ids {
924
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
925
+ font-weight: 600;
926
+ word-break: break-all;
927
+ }
928
+
880
929
  /* ── Tool usages fold-able 박스 — AI 도구 호출 trace ───── */
881
930
  .tool-usages {
882
931
  margin-top: 4px;
@@ -1482,12 +1531,22 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1482
1531
  ? html `<div class="msg-label sender">${line.senderName || i18next.t('board-ai.label.participant')}</div>`
1483
1532
  : nothing}
1484
1533
  <div class="msg-wrap ${line.role} ${isOther ? 'other' : ''}">
1485
- <div class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''}">
1534
+ <div
1535
+ class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''} ${line.systemOpen
1536
+ ? 'open'
1537
+ : ''}">
1486
1538
  ${line.pending
1487
1539
  ? html `<span class="typing-dots"><span></span><span></span><span></span></span>`
1488
1540
  : isMarkdown
1489
1541
  ? html `<div class="md-body">${unsafeHTML(renderMarkdown(content))}</div>`
1490
- : content}
1542
+ : line.role === 'system'
1543
+ ? html `<span
1544
+ class="sys-text"
1545
+ title=${i18next.t('board-ai.text.toggle-system-note', { defaultValue: '눌러서 펼치기' })}
1546
+ @click=${() => this.toggleSystemNote(idx)}
1547
+ >${content}</span
1548
+ >`
1549
+ : content}
1491
1550
  ${line.patchId
1492
1551
  ? html `
1493
1552
  <div class="summary ${this.revertedPatchIds.has(line.patchId) ? 'reverted' : ''}">
@@ -1509,6 +1568,9 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1509
1568
  ${line.followUp
1510
1569
  ? html `<div class="followup">${unsafeHTML(renderMarkdown(line.followUp.trim()))}</div>`
1511
1570
  : nothing}
1571
+ ${line.groundingWarnings && line.groundingWarnings.length > 0
1572
+ ? this.renderGroundingWarning(line)
1573
+ : nothing}
1512
1574
  ${line.toolUsages && line.toolUsages.length > 0
1513
1575
  ? this.renderToolUsages(idx, line)
1514
1576
  : nothing}
@@ -2086,6 +2148,29 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2086
2148
  </div>
2087
2149
  `;
2088
2150
  }
2151
+ /**
2152
+ * 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
2153
+ *
2154
+ * 왜 답을 지우지 않는가: 판정은 식별자 대조라 확정이 아니다(예: 사용자가 화면에서 본 이름을
2155
+ * 대화 밖에서 알고 말한 경우). 그래서 답은 그대로 보여주고, 검증되지 않았다는 사실만 붙인다 —
2156
+ * 사용자가 그 대상을 실제 작업의 근거로 삼기 전에 알아야 하는 정보다.
2157
+ */
2158
+ renderGroundingWarning(line) {
2159
+ const ids = line.groundingWarnings ?? [];
2160
+ if (ids.length === 0)
2161
+ return nothing;
2162
+ return html `
2163
+ <div class="grounding-warning" role="note">
2164
+ <md-icon>report</md-icon>
2165
+ <span>
2166
+ ${i18next.t('board-ai.text.ungrounded-mention', {
2167
+ defaultValue: '확인되지 않은 대상을 언급했습니다 — 실제 데이터에 없습니다:'
2168
+ })}
2169
+ <span class="ids">${ids.join(', ')}</span>
2170
+ </span>
2171
+ </div>
2172
+ `;
2173
+ }
2089
2174
  /**
2090
2175
  * "AI 가 이런 도구를 사용했습니다" fold-able 박스.
2091
2176
  *
@@ -2136,6 +2221,15 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2136
2221
  </div>
2137
2222
  `;
2138
2223
  }
2224
+ /** 시스템 기록 펼치기/접기 — 곁줄이 길어도 대화가 가려지지 않게. */
2225
+ toggleSystemNote(idx) {
2226
+ const next = [...this.lines];
2227
+ const line = next[idx];
2228
+ if (!line)
2229
+ return;
2230
+ next[idx] = { ...line, systemOpen: !line.systemOpen };
2231
+ this.lines = next;
2232
+ }
2139
2233
  toggleToolUsages(idx) {
2140
2234
  const next = [...this.lines];
2141
2235
  const line = next[idx];
@@ -2277,6 +2371,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2277
2371
  hostContext: this.hostContext,
2278
2372
  toolCategories: this.toolCategories,
2279
2373
  boardTools: this.boardTools,
2374
+ requireGroundingTools: this.requireGroundingTools,
2280
2375
  knownTypes: this.knownTypes,
2281
2376
  categories: this.categories,
2282
2377
  componentSchemas: this.componentSchemas,
@@ -2301,6 +2396,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2301
2396
  followUp: out.followUp || undefined,
2302
2397
  patchId: out.patchId || undefined,
2303
2398
  toolUsages: Array.isArray(out.toolUsages) ? out.toolUsages : undefined,
2399
+ groundingWarnings: Array.isArray(out.groundingWarnings) ? out.groundingWarnings : undefined,
2304
2400
  pending: false
2305
2401
  });
2306
2402
  // 호스트로 patch 이벤트 전파
@@ -2384,6 +2480,10 @@ __decorate([
2384
2480
  property({ type: Boolean, attribute: 'board-tools' }),
2385
2481
  __metadata("design:type", Object)
2386
2482
  ], OxBoardAIChat.prototype, "boardTools", void 0);
2483
+ __decorate([
2484
+ property({ type: Boolean, attribute: 'require-grounding-tools' }),
2485
+ __metadata("design:type", Object)
2486
+ ], OxBoardAIChat.prototype, "requireGroundingTools", void 0);
2387
2487
  __decorate([
2388
2488
  property({ type: Array }),
2389
2489
  __metadata("design:type", Array)