@things-factory/ai-assistant 10.1.24 → 10.1.25

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.
@@ -7,8 +7,8 @@
7
7
  * - scopes / knownTypes / categories: 도메인 컨텍스트
8
8
  *
9
9
  * 출력 (이벤트):
10
- * - `board-edit-patch` { detail: { patch, summary, confidence, patchId } }
11
- * 호스트가 받아서 보드 모델에 적용 (applyBoardEditPatch helper).
10
+ * - `scene-edit-patch` { detail: { patch, summary, confidence, patchId } }
11
+ * 호스트가 받아서 보드 모델에 적용 (applyScenePatch from @operato/scene-ops).
12
12
  * - `chat-followup` { detail: { question } }
13
13
  *
14
14
  * 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
@@ -59,6 +59,7 @@ import {
59
59
  * 뜨지 않는 것이 맞다.
60
60
  */
61
61
  import { chatDefaults } from './chat-defaults.js'
62
+ import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode.js'
62
63
  import { availableTriggers, type TriggerChip } from './chat-triggers.js'
63
64
 
64
65
  interface ChatLine {
@@ -365,6 +366,18 @@ export class OxAssistantChat extends LitElement {
365
366
  * Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
366
367
  * board conversation carries its own persona on the server and ignores this.
367
368
  */
369
+ /**
370
+ * What this conversation is — see `AssistantMode`.
371
+ *
372
+ * A host that sets this does not set `systemPrompt`, `toolCategories`, `hostContext` or
373
+ * `chatEndpoint` separately, and does not rely on `registerChatDefaults` for its `@` and `/`
374
+ * lists: the mode carries all of them together, so they cannot disagree with each other.
375
+ *
376
+ * When `mode.id` changes the conversation on screen is closed and a new one begins.
377
+ */
378
+ @property({ attribute: false })
379
+ mode?: AssistantMode
380
+
368
381
  @property({ type: String, attribute: 'system-prompt' })
369
382
  systemPrompt?: string
370
383
 
@@ -594,7 +607,7 @@ export class OxAssistantChat extends LitElement {
594
607
  The screen still shows nothing either way. There is no sentence to show, and inventing one is
595
608
  what this whole correction removed.
596
609
  */
597
- if (!chatDefaults.stagedNoticeKey && !OxAssistantChat.warnedMissingStagedNotice) {
610
+ if (!this.conversation.stagedNoticeKey && !OxAssistantChat.warnedMissingStagedNotice) {
598
611
  OxAssistantChat.warnedMissingStagedNotice = true
599
612
  console.warn(
600
613
  '[assistant-chat] a host reported a proposal staged but registered no `stagedNoticeKey`, ' +
@@ -692,12 +705,40 @@ export class OxAssistantChat extends LitElement {
692
705
  /** centerTo 디바운스 timer — arrow nav / hover 빠를 때 카메라 애니메이션 race 방지 */
693
706
  private centerDebounceTimer?: ReturnType<typeof setTimeout>
694
707
 
695
- /** Default 카탈로그 후보 캐시 — chat panel mount 시 1회 빌드 */
708
+ /*
709
+ * ── What this conversation is, resolved ──────────────────────────────────────────────
710
+ *
711
+ * A mode answers first. A host that has not declared one keeps the separate properties and
712
+ * the global defaults it had before, which is what every consumer written before modes does.
713
+ */
714
+ private get conversation() {
715
+ return resolveConversation(
716
+ this.mode,
717
+ {
718
+ systemPrompt: this.systemPrompt,
719
+ toolCategories: this.toolCategories,
720
+ hostContext: this.hostContext,
721
+ chatEndpoint: this.chatEndpoint
722
+ },
723
+ chatDefaults
724
+ )
725
+ }
726
+
727
+ /*
728
+ * The `@` and `/` candidates, built once per mode.
729
+ *
730
+ * These were built at field initialisation, which meant a host could not change what the
731
+ * panel offers without replacing the element. A mode change rebuilds them below.
732
+ */
696
733
  private cachedCatalogCandidates: MentionCandidate[] = buildCatalogCandidates(chatDefaults.catalogEntries)
697
734
 
698
- /** Default slash template 후보 캐시 — chat panel mount 시 1회 빌드 */
699
735
  private cachedSlashCandidates: MentionCandidate[] = buildSlashCandidates(chatDefaults.slashTemplates)
700
736
 
737
+ private rebuildMentionCaches() {
738
+ this.cachedCatalogCandidates = buildCatalogCandidates(this.conversation.catalogEntries)
739
+ this.cachedSlashCandidates = buildSlashCandidates(this.conversation.slashTemplates)
740
+ }
741
+
701
742
  /** `@` 마지막 query 의 도메인 사용자 후보 캐시 — userProvider 응답 도착 시 갱신.
702
743
  * query 별 캐시가 아니라 단순 "최근 결과" — async race 회피. */
703
744
  private cachedUserCandidates: MentionCandidate[] = []
@@ -2214,6 +2255,36 @@ export class OxAssistantChat extends LitElement {
2214
2255
  this.send()
2215
2256
  }
2216
2257
 
2258
+ /*
2259
+ * The subject changed, so the conversation is over.
2260
+ *
2261
+ * A conversation about station 3 is not a conversation about station 7. Leaving the old one
2262
+ * on screen means the user reads an answer about a screen they are no longer looking at,
2263
+ * and every host that met this wrote its own guard: plant re-checked the context on each
2264
+ * send and threw when it had moved, twin closed its session when the anchor changed. The
2265
+ * mode carries an id, so the component can do it once, here.
2266
+ *
2267
+ * The host still owns `sessionId`; it hears `assistant-mode-changed` and decides whether
2268
+ * this subject gets a stored session of its own.
2269
+ */
2270
+ if (changed.has('mode') && isDifferentConversation(changed.get('mode'), this.mode)) {
2271
+ this.rebuildMentionCaches()
2272
+ this.lines = []
2273
+ this.pickedMentions.clear()
2274
+ this.pickedUserMentions.clear()
2275
+ this.cachedUserCandidates = []
2276
+ this.errorMessage = undefined
2277
+ this.input = ''
2278
+ this._seenMessageIds.clear()
2279
+ this.dispatchEvent(
2280
+ new CustomEvent('assistant-mode-changed', {
2281
+ detail: { mode: this.mode },
2282
+ bubbles: true,
2283
+ composed: true
2284
+ })
2285
+ )
2286
+ }
2287
+
2217
2288
  if (changed.has('sessionId') && this.adoptingSession) {
2218
2289
  this.adoptingSession = false
2219
2290
  this._startPresence()
@@ -2815,7 +2886,7 @@ export class OxAssistantChat extends LitElement {
2815
2886
 
2816
2887
  private dispatchActions(actions: any[]) {
2817
2888
  this.dispatchEvent(
2818
- new CustomEvent('board-action-execute', {
2889
+ new CustomEvent('scene-action-execute', {
2819
2890
  detail: { actions, sessionId: this.sessionId },
2820
2891
  bubbles: true,
2821
2892
  composed: true
@@ -2946,7 +3017,7 @@ export class OxAssistantChat extends LitElement {
2946
3017
  return availableTriggers({
2947
3018
  hasMentionableThings: !!(this.boardProvider || this.currentBoard || this.knownTypes?.length),
2948
3019
  hasSession: !!this.sessionId,
2949
- slashTemplateCount: chatDefaults.slashTemplates.length
3020
+ slashTemplateCount: this.conversation.slashTemplates.length
2950
3021
  })
2951
3022
  }
2952
3023
 
@@ -3082,8 +3153,8 @@ export class OxAssistantChat extends LitElement {
3082
3153
  <span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
3083
3154
  <span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
3084
3155
  <span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
3085
- ${chatDefaults.footerNoticeKey
3086
- ? html`<br />${i18next.t(chatDefaults.footerNoticeKey)}`
3156
+ ${this.conversation.footerNoticeKey
3157
+ ? html`<br />${i18next.t(this.conversation.footerNoticeKey)}`
3087
3158
  : ''}
3088
3159
  </div>
3089
3160
  </div>
@@ -3242,7 +3313,7 @@ export class OxAssistantChat extends LitElement {
3242
3313
  to live here named a 3D modeller's operations in hardcoded Korean, and it reached the
3243
3314
  plant and twin chats too.
3244
3315
  */
3245
- const rawChoices: any[] = Array.isArray(p.choices) ? p.choices : (chatDefaults.proposalChoices ?? [])
3316
+ const rawChoices: any[] = Array.isArray(p.choices) ? p.choices : this.conversation.proposalChoices
3246
3317
 
3247
3318
  return html`
3248
3319
  <div class="proposal ${sent ? 'sent' : ''}">
@@ -3271,10 +3342,10 @@ export class OxAssistantChat extends LitElement {
3271
3342
  This badge used to read "there are chips" as "the model is in the 3D viewer" and
3272
3343
  say so, in a component that plant and twin also use.
3273
3344
  */ ''}
3274
- ${this.stagedProposals.has(key) && chatDefaults.stagedNoticeKey ? html`
3345
+ ${this.stagedProposals.has(key) && this.conversation.stagedNoticeKey ? html`
3275
3346
  <span class="staged-badge">
3276
3347
  <md-icon>check_circle</md-icon>
3277
- <span>${i18next.t(chatDefaults.stagedNoticeKey)}</span>
3348
+ <span>${i18next.t(this.conversation.stagedNoticeKey!)}</span>
3278
3349
  </span>
3279
3350
  ` : nothing}
3280
3351
  </div>
@@ -3668,7 +3739,7 @@ export class OxAssistantChat extends LitElement {
3668
3739
  let requestBase: any
3669
3740
  try {
3670
3741
  const provided = this.contextProvider?.() || {}
3671
- requestContext = structuredClone({ systemPrompt: this.systemPrompt, hostContext: this.hostContext, ...provided })
3742
+ requestContext = structuredClone({ systemPrompt: this.conversation.systemPrompt, hostContext: this.conversation.hostContext, ...provided })
3672
3743
  liveBoard = structuredClone(this.boardProvider ? this.boardProvider() : this.currentBoard)
3673
3744
  requestBase = { version: 1, sessionId: this.sessionId, hostContext: requestContext.hostContext,
3674
3745
  modelFingerprint: assistantModelFingerprint(liveBoard) }
@@ -3745,7 +3816,7 @@ export class OxAssistantChat extends LitElement {
3745
3816
  // 라이브 보드 우선 — 호스트의 캔버스가 사용자 수작업 편집을 들고 있을 수 있음.
3746
3817
  // boardProvider 가 있으면 send 시점에 그것을 pull, 없으면 정적 currentBoard.
3747
3818
  // mentions 는 위에서 이미 계산해 user line 에 thread — 그대로 mutation 입력으로 재사용.
3748
- const neutral = this.chatEndpoint === 'assistantChat'
3819
+ const neutral = this.conversation.chatEndpoint === 'assistantChat'
3749
3820
  const result = await client.mutate({
3750
3821
  mutation: neutral ? ASSISTANT_CHAT_MUTATION : BOARD_AI_CHAT_MUTATION,
3751
3822
  variables: {
@@ -3756,7 +3827,7 @@ export class OxAssistantChat extends LitElement {
3756
3827
  systemPrompt: requestContext.systemPrompt!,
3757
3828
  truncateAfterMessageId: this._truncateAfterId,
3758
3829
  hostContext: requestContext.hostContext,
3759
- toolCategories: this.toolCategories,
3830
+ toolCategories: this.conversation.toolCategories,
3760
3831
  requireGroundingTools: this.requireGroundingTools
3761
3832
  })
3762
3833
  : buildChatMutationInput({
@@ -3766,7 +3837,7 @@ export class OxAssistantChat extends LitElement {
3766
3837
  scopes: this.scopes,
3767
3838
  truncateAfterMessageId: this._truncateAfterId,
3768
3839
  hostContext: requestContext.hostContext,
3769
- toolCategories: this.toolCategories,
3840
+ toolCategories: this.conversation.toolCategories,
3770
3841
  boardTools: this.boardTools,
3771
3842
  requireGroundingTools: this.requireGroundingTools,
3772
3843
  knownTypes: this.knownTypes,
@@ -3821,7 +3892,7 @@ export class OxAssistantChat extends LitElement {
3821
3892
  // 호스트로 patch 이벤트 전파
3822
3893
  if (out.patch) {
3823
3894
  this.dispatchEvent(
3824
- new CustomEvent('board-edit-patch', {
3895
+ new CustomEvent('scene-edit-patch', {
3825
3896
  detail: {
3826
3897
  patch: out.patch,
3827
3898
  requestBase,
@@ -3838,7 +3909,7 @@ export class OxAssistantChat extends LitElement {
3838
3909
  // 호스트로 ephemeral scene 조작 actions 전파 (selection / view / mode)
3839
3910
  if (Array.isArray(out.actions) && out.actions.length > 0) {
3840
3911
  this.dispatchEvent(
3841
- new CustomEvent('board-action-execute', {
3912
+ new CustomEvent('scene-action-execute', {
3842
3913
  detail: { actions: out.actions, sessionId: out.sessionId, requestBase },
3843
3914
  bubbles: true,
3844
3915
  composed: true
@@ -0,0 +1,82 @@
1
+ /*
2
+ * A mode is one value, and the point of it is that the parts of a conversation cannot
3
+ * disagree with each other.
4
+ *
5
+ * The defect this guards against shipped: a screen whose systemPrompt said "you do not edit
6
+ * boards here" offered `/add-monitor` in the same panel, because the prompt was an element
7
+ * property and the slash list was a module global. Any resolution that lets a mode supply one
8
+ * of them while something else supplies the other brings it back.
9
+ */
10
+ import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode'
11
+
12
+ const MODE: AssistantMode = {
13
+ id: 'twin:space:S-12',
14
+ systemPrompt: 'You are looking at one space. You do not edit boards here.',
15
+ toolCategories: ['twin'],
16
+ hostContext: { spaceId: 'S-12' },
17
+ slashTemplates: [{ name: 'status', template: '이 공간의 지금 상태를 요약해줘' }]
18
+ }
19
+
20
+ const HOST_PROPERTIES = {
21
+ systemPrompt: 'a prompt the host set the old way',
22
+ toolCategories: ['board-ai'],
23
+ hostContext: { boardId: 'B-1' },
24
+ chatEndpoint: 'boardAIChat' as const
25
+ }
26
+
27
+ const GLOBAL_DEFAULTS = {
28
+ catalogEntries: [{ type: 'rect' }],
29
+ slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],
30
+ footerNoticeKey: 'text.previewed-on-the-board'
31
+ }
32
+
33
+ describe('resolveConversation', () => {
34
+ test('a mode answers before the host properties it overlaps', () => {
35
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
36
+ expect(c.systemPrompt).toBe(MODE.systemPrompt)
37
+ expect(c.toolCategories).toEqual(['twin'])
38
+ expect(c.hostContext).toEqual({ spaceId: 'S-12' })
39
+ })
40
+
41
+ test('a mode answers before the global defaults — the prompt and the slash list cannot disagree', () => {
42
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
43
+ expect(c.slashTemplates).toEqual(MODE.slashTemplates)
44
+ /* A mode that says it does not edit boards must not offer a board edit. */
45
+ expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')
46
+ })
47
+
48
+ test('what a mode leaves out still comes from the defaults', () => {
49
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
50
+ expect(c.catalogEntries).toEqual(GLOBAL_DEFAULTS.catalogEntries)
51
+ expect(c.footerNoticeKey).toBe(GLOBAL_DEFAULTS.footerNoticeKey)
52
+ })
53
+
54
+ test('a host with no mode gets exactly what it had before modes existed', () => {
55
+ const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)
56
+ expect(c.systemPrompt).toBe(HOST_PROPERTIES.systemPrompt)
57
+ expect(c.toolCategories).toEqual(HOST_PROPERTIES.toolCategories)
58
+ expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)
59
+ })
60
+
61
+ test('nothing anywhere resolves to empty lists and the board endpoint, never undefined', () => {
62
+ const c = resolveConversation(undefined, {}, {})
63
+ expect(c.catalogEntries).toEqual([])
64
+ expect(c.slashTemplates).toEqual([])
65
+ expect(c.proposalChoices).toEqual([])
66
+ expect(c.chatEndpoint).toBe('boardAIChat')
67
+ })
68
+ })
69
+
70
+ describe('isDifferentConversation', () => {
71
+ const station3: AssistantMode = { id: 'plant:station:3' }
72
+
73
+ test('a different subject ends the conversation', () => {
74
+ expect(isDifferentConversation(station3, { id: 'plant:station:7' })).toBe(true)
75
+ expect(isDifferentConversation(undefined, station3)).toBe(true)
76
+ })
77
+
78
+ test('rewording the same subject does not', () => {
79
+ expect(isDifferentConversation(station3, { ...station3, systemPrompt: 'reworded' })).toBe(false)
80
+ expect(isDifferentConversation(undefined, undefined)).toBe(false)
81
+ })
82
+ })
@@ -0,0 +1,124 @@
1
+ /**
2
+ * A mode is what a conversation *is*: who the assistant is, what it can do, what the talk is
3
+ * about, what it offers to start with, and when it is over.
4
+ *
5
+ * Those five used to be four separate properties on the chat element plus one global registry,
6
+ * and every host wired them one at a time. Separate means they can disagree, and they did: a
7
+ * screen whose `systemPrompt` said "you do not edit boards here" offered `/add-monitor` in the
8
+ * same panel, because the prompt was a property and the slash list was global.
9
+ *
10
+ * So a host declares one value and hands it over. What cannot be split cannot fall out of step.
11
+ *
12
+ * ```ts
13
+ * const SPACE_MODE = (spaceId: string): AssistantMode => ({
14
+ * id: `twin:space:${spaceId}`,
15
+ * systemPrompt: '...',
16
+ * toolCategories: ['twin'],
17
+ * hostContext: { spaceId },
18
+ * slashTemplates: TWIN_SLASH_TEMPLATES
19
+ * })
20
+ *
21
+ * html`<ox-assistant-chat .mode=${SPACE_MODE(this.spaceId)}></ox-assistant-chat>`
22
+ * ```
23
+ */
24
+ export interface AssistantMode {
25
+ /**
26
+ * What this conversation is about, as a string the host can compare.
27
+ *
28
+ * **The conversation's lifetime hangs on this.** When it changes, the chat closes what is on
29
+ * screen and starts again — because a conversation about station 3 is not a conversation
30
+ * about station 7, and leaving the old one up means the user reads an answer about a screen
31
+ * they are no longer looking at.
32
+ *
33
+ * Hosts built this by hand before, each in their own way: plant kept a module-global registry
34
+ * with subscribers and re-checked the context on every send, twin closed its session when the
35
+ * anchor moved. Both were writing the same guard around the same absence.
36
+ *
37
+ * Include whatever makes the subject specific — `twin:space:S-12`, `plant:station:3`.
38
+ */
39
+ id: string
40
+
41
+ /** Who the assistant is here, and what it must not claim to do. */
42
+ systemPrompt?: string
43
+
44
+ /** Which tool categories the server may expose for this conversation. */
45
+ toolCategories?: string[]
46
+
47
+ /** What the host knows about the subject; travels with each request. */
48
+ hostContext?: any
49
+
50
+ /** Which server-side chat entry point answers. */
51
+ chatEndpoint?: 'boardAIChat' | 'assistantChat'
52
+
53
+ /** What `@` offers. */
54
+ catalogEntries?: any[]
55
+
56
+ /**
57
+ * What `/` offers.
58
+ *
59
+ * These are sentences a person could have typed, not tool names — "이 공간의 지금 상태를
60
+ * 요약해줘" cannot be generated from `summarizeStatus`. But **a mode must not offer work it
61
+ * cannot do**: everything here should be answerable by the tools `toolCategories` opens.
62
+ */
63
+ slashTemplates?: any[]
64
+
65
+ /** The i18n key for the line under an empty conversation. */
66
+ footerNoticeKey?: string
67
+
68
+ /** Follow-up chips for a proposal that arrived without its own. */
69
+ proposalChoices?: any[]
70
+
71
+ /** The i18n key for the line shown once the host reports a proposal staged. */
72
+ stagedNoticeKey?: string
73
+ }
74
+
75
+ /** True when these two describe different conversations, so the old one should be closed. */
76
+ export function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {
77
+ return (a?.id ?? '') !== (b?.id ?? '')
78
+ }
79
+
80
+ /** Everything a conversation needs, after the mode, the host's own properties and the
81
+ * registered defaults have been reconciled. */
82
+ export interface ConversationSettings {
83
+ systemPrompt?: string
84
+ toolCategories?: string[]
85
+ hostContext?: any
86
+ chatEndpoint: 'boardAIChat' | 'assistantChat'
87
+ catalogEntries: any[]
88
+ slashTemplates: any[]
89
+ footerNoticeKey?: string
90
+ proposalChoices: any[]
91
+ stagedNoticeKey?: string
92
+ }
93
+
94
+ /**
95
+ * Reconcile the three places a setting can come from.
96
+ *
97
+ * **The mode answers first, as a whole.** Then the host's own properties, then whatever was
98
+ * registered globally. The order matters in one direction only: a host that declares a mode
99
+ * must not have half of its conversation decided somewhere else, which is the failure this
100
+ * type exists to remove.
101
+ *
102
+ * A host that declares no mode gets exactly what it got before modes existed — its own
103
+ * properties, falling back to the registered defaults.
104
+ */
105
+ export function resolveConversation(
106
+ mode: AssistantMode | undefined,
107
+ own: Partial<ConversationSettings>,
108
+ defaults: Partial<ConversationSettings>
109
+ ): ConversationSettings {
110
+ const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>
111
+ (mode as any)?.[key] ?? (own as any)[key] ?? (defaults as any)[key]
112
+
113
+ return {
114
+ systemPrompt: pick('systemPrompt'),
115
+ toolCategories: pick('toolCategories'),
116
+ hostContext: pick('hostContext'),
117
+ chatEndpoint: pick('chatEndpoint') ?? 'boardAIChat',
118
+ catalogEntries: pick('catalogEntries') ?? [],
119
+ slashTemplates: pick('slashTemplates') ?? [],
120
+ footerNoticeKey: pick('footerNoticeKey'),
121
+ proposalChoices: pick('proposalChoices') ?? [],
122
+ stagedNoticeKey: pick('stagedNoticeKey')
123
+ }
124
+ }
package/client/index.ts CHANGED
@@ -5,12 +5,12 @@
5
5
  */
6
6
  export * from './components/assistant-chat.js'
7
7
  export * from './components/chat-defaults.js'
8
+ export * from './components/assistant-mode.js'
8
9
  export * from './components/markdown.js'
9
10
  export * from './components/chat-echo-dedup.js'
10
11
  export * from './components/chat-input-builder.js'
11
12
  export * from './components/mention-popup.js'
12
13
  export * from './components/mention-popup-helpers.js'
13
- export * from './utils/board-edit-patch.js'
14
14
  export * from './utils/assistant-session-controller.js'
15
15
  export * from './utils/assistant-session-transport.js'
16
16
  export * from './components/assistant-session-toolbar.js'
@@ -7,8 +7,8 @@
7
7
  * - scopes / knownTypes / categories: 도메인 컨텍스트
8
8
  *
9
9
  * 출력 (이벤트):
10
- * - `board-edit-patch` { detail: { patch, summary, confidence, patchId } }
11
- * 호스트가 받아서 보드 모델에 적용 (applyBoardEditPatch helper).
10
+ * - `scene-edit-patch` { detail: { patch, summary, confidence, patchId } }
11
+ * 호스트가 받아서 보드 모델에 적용 (applyScenePatch from @operato/scene-ops).
12
12
  * - `chat-followup` { detail: { question } }
13
13
  *
14
14
  * 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
@@ -19,6 +19,7 @@ import { type AssistantRequestContext } from '../utils/assistant-request-context
19
19
  import { LitElement } from 'lit';
20
20
  import './mention-popup.js';
21
21
  import { type FilterResult } from './mention-popup.js';
22
+ import { type AssistantMode } from './assistant-mode.js';
22
23
  export interface ChatAttachment {
23
24
  id: string;
24
25
  file: File;
@@ -118,6 +119,16 @@ export declare class OxAssistantChat extends LitElement {
118
119
  * Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
119
120
  * board conversation carries its own persona on the server and ignores this.
120
121
  */
122
+ /**
123
+ * What this conversation is — see `AssistantMode`.
124
+ *
125
+ * A host that sets this does not set `systemPrompt`, `toolCategories`, `hostContext` or
126
+ * `chatEndpoint` separately, and does not rely on `registerChatDefaults` for its `@` and `/`
127
+ * lists: the mode carries all of them together, so they cannot disagree with each other.
128
+ *
129
+ * When `mode.id` changes the conversation on screen is closed and a new one begins.
130
+ */
131
+ mode?: AssistantMode;
121
132
  systemPrompt?: string;
122
133
  /**
123
134
  * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
@@ -279,10 +290,10 @@ export declare class OxAssistantChat extends LitElement {
279
290
  private inputDebounceTimer?;
280
291
  /** centerTo 디바운스 timer — arrow nav / hover 빠를 때 카메라 애니메이션 race 방지 */
281
292
  private centerDebounceTimer?;
282
- /** Default 카탈로그 후보 캐시 — chat panel mount 시 1회 빌드 */
293
+ private get conversation();
283
294
  private cachedCatalogCandidates;
284
- /** Default slash template 후보 캐시 — chat panel mount 시 1회 빌드 */
285
295
  private cachedSlashCandidates;
296
+ private rebuildMentionCaches;
286
297
  /** `@` 마지막 query 의 도메인 사용자 후보 캐시 — userProvider 응답 도착 시 갱신.
287
298
  * query 별 캐시가 아니라 단순 "최근 결과" — async race 회피. */
288
299
  private cachedUserCandidates;