@things-factory/ai-assistant 10.1.8 → 10.1.15

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 (58) hide show
  1. package/client/components/{board-ai-chat.ts → assistant-chat.ts} +597 -66
  2. package/client/components/assistant-session-toolbar.ts +70 -0
  3. package/client/components/chat-input-builder.ts +1 -1
  4. package/client/index.ts +6 -3
  5. package/client/utils/assistant-request-context.ts +21 -0
  6. package/client/utils/assistant-session-controller.ts +166 -0
  7. package/client/utils/assistant-session-transport.ts +34 -0
  8. package/dist-client/components/{board-ai-chat.d.ts → assistant-chat.d.ts} +30 -3
  9. package/dist-client/components/{board-ai-chat.js → assistant-chat.js} +616 -109
  10. package/dist-client/components/assistant-chat.js.map +1 -0
  11. package/dist-client/components/{board-ai-chat.test.js → assistant-chat.test.js} +1 -1
  12. package/dist-client/components/assistant-chat.test.js.map +1 -0
  13. package/dist-client/components/assistant-session-toolbar.d.ts +18 -0
  14. package/dist-client/components/assistant-session-toolbar.js +109 -0
  15. package/dist-client/components/assistant-session-toolbar.js.map +1 -0
  16. package/dist-client/components/chat-input-builder.js +1 -1
  17. package/dist-client/components/chat-input-builder.js.map +1 -1
  18. package/dist-client/index.d.ts +6 -3
  19. package/dist-client/index.js +6 -3
  20. package/dist-client/index.js.map +1 -1
  21. package/dist-client/tsconfig.tsbuildinfo +1 -1
  22. package/dist-client/utils/assistant-request-context.d.ts +10 -0
  23. package/dist-client/utils/assistant-request-context.js +20 -0
  24. package/dist-client/utils/assistant-request-context.js.map +1 -0
  25. package/dist-client/utils/assistant-session-controller.d.ts +49 -0
  26. package/dist-client/utils/assistant-session-controller.js +165 -0
  27. package/dist-client/utils/assistant-session-controller.js.map +1 -0
  28. package/dist-client/utils/assistant-session-transport.d.ts +3 -0
  29. package/dist-client/utils/assistant-session-transport.js +39 -0
  30. package/dist-client/utils/assistant-session-transport.js.map +1 -0
  31. package/dist-server/service/assistant-chat-resolver.js +20 -0
  32. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  33. package/dist-server/service/chat-session/chat-session.js +1 -1
  34. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  35. package/dist-server/service/chat-session/session-anchor.d.ts +1 -1
  36. package/dist-server/service/chat-session/session-anchor.js +3 -1
  37. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  38. package/dist-server/service/chat-session/station-session-guard.d.ts +12 -0
  39. package/dist-server/service/chat-session/station-session-guard.js +20 -0
  40. package/dist-server/service/chat-session/station-session-guard.js.map +1 -0
  41. package/dist-server/tsconfig.tsbuildinfo +1 -1
  42. package/docs/session-controller.md +52 -0
  43. package/package.json +2 -2
  44. package/server/service/assistant-chat-resolver.ts +22 -0
  45. package/server/service/chat-session/chat-session.ts +1 -1
  46. package/server/service/chat-session/session-anchor.ts +3 -1
  47. package/server/service/chat-session/station-session-guard.ts +17 -0
  48. package/server/service/session-anchor.test.ts +11 -0
  49. package/server/service/station-session-guard.test.ts +24 -0
  50. package/test/assistant-request-context.test.ts +30 -0
  51. package/test/assistant-session-controller.test.ts +88 -0
  52. package/test/assistant-session-toolbar.test.ts +29 -0
  53. package/translations/en.json +12 -1
  54. package/translations/ko.json +12 -1
  55. package/dist-client/components/board-ai-chat.js.map +0 -1
  56. package/dist-client/components/board-ai-chat.test.js.map +0 -1
  57. /package/client/components/{board-ai-chat.test.ts → assistant-chat.test.ts} +0 -0
  58. /package/dist-client/components/{board-ai-chat.test.d.ts → assistant-chat.test.d.ts} +0 -0
@@ -0,0 +1,70 @@
1
+ import { LitElement, css, html, nothing } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import { i18next } from '@operato/i18n'
4
+ import type { AssistantSession } from '../utils/assistant-session-controller'
5
+ import { assistantRecentSessions } from '../utils/assistant-request-context.js'
6
+
7
+ /** Product-neutral controls. The host/controller owns effects; this view never mutates sessions. */
8
+ @customElement('ox-assistant-session-toolbar')
9
+ export class AssistantSessionToolbar extends LitElement {
10
+ @property({ attribute: false }) sessions: AssistantSession[] = []
11
+ @property() sessionId = ''
12
+ @property({ type: Boolean }) loading = false
13
+ @property({ type: Boolean }) disabled = false
14
+ @property({ type: Boolean }) temporary = false
15
+ @property() error = ''
16
+ @property({ type: Boolean }) historyEnabled = false
17
+ @property({ type: Number }) maxTabs = 5
18
+ /** 좁은 도크의 문맥 헤더 바로 아래에서 쓰는 밀도. 기능은 줄이지 않는다. */
19
+ @property({ type: Boolean, reflect: true }) compact = false
20
+
21
+ static styles = css`
22
+ :host { display: block; min-width: 0; }
23
+ .controls { display: flex; align-items: center; gap: 6px; }
24
+ button { box-sizing: border-box; height: 28px; font: inherit; font-size: 12px;
25
+ color: var(--secondary-color, #68747c); border: 0; border-radius: 4px;
26
+ background: transparent; cursor: pointer; }
27
+ .tabs { display:flex; flex:1; min-width:0; overflow-x:auto; gap:1px; scrollbar-width:thin; }
28
+ [role=tab] { max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
29
+ padding: 0 8px; border-bottom: 2px solid transparent; border-radius: 3px 3px 0 0; }
30
+ [role=tab]:hover { background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent); color: var(--primary-color, #007f87); }
31
+ [aria-selected=true] { color:var(--primary-color, #007f87); border-bottom-color: currentColor; font-weight:600; }
32
+ .session-new { flex: none; padding: 0 7px; font-weight: 500; white-space: nowrap; }
33
+ .session-new:hover, .history:hover { color: var(--primary-color, #007f87); background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent); }
34
+ .history { height: 24px; margin: 1px 0 0 4px; padding: 0 5px; font-size: 11px; color: var(--tertiary-color, #88939a); }
35
+ :host([compact]) .controls { gap: 3px; min-height: 24px; }
36
+ :host([compact]) button { height: 24px; font-size: 11px; }
37
+ :host([compact]) [role=tab] { max-width: 132px; padding: 0 6px; border-bottom-width: 1px; }
38
+ :host([compact]) .session-new { padding: 0 5px; }
39
+ :disabled { opacity: .5; cursor: default; }
40
+ :focus-visible { outline: 2px solid var(--primary-color, #007f87); outline-offset: 2px; }
41
+ p { margin: 6px 0 0; font-size: 12px; overflow-wrap: anywhere; }
42
+ [role=alert] { color: var(--error-color, #b42318); }
43
+ `
44
+
45
+ private emit(name: string, detail?: unknown) {
46
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }))
47
+ }
48
+
49
+ render() {
50
+ const label = i18next.t('ai-assistant.session.history', { defaultValue: '대화 이력' })
51
+ return html`
52
+ <div class="controls">
53
+ ${this.temporary ? nothing : html`<div class="tabs" role="tablist" aria-label=${label}>
54
+ ${!this.sessionId ? html`<button role="tab" aria-selected="true">${i18next.t('ai-assistant.session.new', { defaultValue: '새 대화' })}</button>` : nothing}
55
+ ${assistantRecentSessions(this.sessions, this.sessionId, this.historyEnabled ? this.maxTabs : this.sessions.length).map((session, index) => html`<button role="tab" aria-selected=${session.id === this.sessionId ? 'true' : 'false'}
56
+ ?disabled=${this.disabled || this.loading} title=${session.name || `${label} ${index + 1}`}
57
+ @click=${() => this.emit('assistant-session-select', { sessionId: session.id })}>${session.name || `${label} ${index + 1}`}</button>`)}
58
+ </div>`}
59
+ <button class="session-new" ?disabled=${this.disabled || this.loading || !!this.error}
60
+ title=${i18next.t('ai-assistant.session.reset-help', { defaultValue: '이전 이력과 작업 내용은 보존하고 새 대화를 시작합니다. 이전 대화는 다음 요청에 포함하지 않습니다.' })}
61
+ @click=${() => this.emit('assistant-session-reset')}>+ ${i18next.t('ai-assistant.session.new', { defaultValue: '새 대화' })}</button>
62
+ </div>
63
+ ${this.historyEnabled ? html`<button class="history" ?disabled=${this.loading || this.disabled}
64
+ @click=${() => this.emit('assistant-session-history')}>${i18next.t('ai-assistant.session.all', { defaultValue: '전체 이력' })}</button>` : nothing}
65
+ ${this.loading ? html`<p role="status">${i18next.t('ai-assistant.session.loading', { defaultValue: '대화 이력을 불러오는 중…' })}</p>` : nothing}
66
+ ${this.error ? html`<p role="alert">${this.error} <button ?disabled=${this.loading || this.disabled}
67
+ @click=${() => this.emit('assistant-session-retry')}>${i18next.t('ai-assistant.session.retry', { defaultValue: '다시 시도' })}</button></p>` : nothing}
68
+ `
69
+ }
70
+ }
@@ -96,7 +96,7 @@ export interface AssistantChatInputArgs {
96
96
  */
97
97
  export function buildAssistantChatInput(args: AssistantChatInputArgs): Record<string, any> {
98
98
  if (!args.systemPrompt?.trim()) {
99
- throw new Error('[ox-board-ai-chat] systemPrompt is required when the chat talks to assistantChat')
99
+ throw new Error('[ox-assistant-chat] systemPrompt is required when the chat talks to assistantChat')
100
100
  }
101
101
 
102
102
  return {
package/client/index.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * `@things-factory/ai-assistant` — 대화면.
3
3
  *
4
- * `<ox-board-ai-chat>` 은 **이름을 안 바꿨다.** 커스텀 엘리먼트 이름은 소비처 여덟 곳의 템플릿에
5
- * 박혀 있고, 그것을 같은 커밋에서 함께 바꾸면 무엇이 깨졌는지 못 가린다. 이름은 별도로 옮긴다.
4
+ * 표준 컴포넌트는 `<ox-assistant-chat>` 이다.
6
5
  */
7
- export * from './components/board-ai-chat.js'
6
+ export * from './components/assistant-chat.js'
8
7
  export * from './components/chat-defaults.js'
9
8
  export * from './components/markdown.js'
10
9
  export * from './components/chat-echo-dedup.js'
@@ -12,3 +11,7 @@ export * from './components/chat-input-builder.js'
12
11
  export * from './components/mention-popup.js'
13
12
  export * from './components/mention-popup-helpers.js'
14
13
  export * from './utils/board-edit-patch.js'
14
+ export * from './utils/assistant-session-controller.js'
15
+ export * from './utils/assistant-session-transport.js'
16
+ export * from './components/assistant-session-toolbar.js'
17
+ export * from './utils/assistant-request-context.js'
@@ -0,0 +1,21 @@
1
+ /** Exact deterministic model signature, not a security hash. Only plain JSON models are supported. */
2
+ export function assistantModelFingerprint(model: unknown): string | undefined {
3
+ if (model === undefined) return undefined
4
+ const normalize = (value: any): any => Array.isArray(value) ? value.map(normalize)
5
+ : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, normalize(value[key])])) : value
6
+ return JSON.stringify(normalize(model))
7
+ }
8
+ export interface AssistantRequestContext {
9
+ systemPrompt?: string
10
+ hostContext?: Record<string, unknown>
11
+ }
12
+ export function assistantAutoAskKey(text: string, requestId?: string): string {
13
+ return requestId ? JSON.stringify([requestId, text]) : text
14
+ }
15
+ export function assistantRecentSessions<T extends { id: string }>(sessions: T[], selected: string, limit: number): T[] {
16
+ const count = Math.max(1, Math.floor(limit) || 5)
17
+ const recent = sessions.slice(0, count)
18
+ const active = sessions.find(session => session.id === selected)
19
+ if (active && !recent.some(session => session.id === selected)) recent[recent.length - 1] = active
20
+ return recent
21
+ }
@@ -0,0 +1,166 @@
1
+ export interface AssistantSession { id: string; name?: string }
2
+ export interface AssistantSessionTarget {
3
+ /** Account/domain boundary supplied by the authenticated host. */
4
+ scope: string
5
+ anchorType: string
6
+ anchorId: string
7
+ }
8
+ export interface AssistantSessionTransport {
9
+ list(target: AssistantSessionTarget): Promise<AssistantSession[]>
10
+ create(target: AssistantSessionTarget): Promise<AssistantSession>
11
+ isEmpty?(sessionId: string): Promise<boolean>
12
+ }
13
+ export interface AssistantSessionState {
14
+ target?: AssistantSessionTarget
15
+ sessions: AssistantSession[]
16
+ sessionId: string
17
+ loading: boolean
18
+ error: string
19
+ /** Changes on reset, including temporary conversations without a server ID. */
20
+ revision: number
21
+ }
22
+ const keyOf = (target?: AssistantSessionTarget) => target
23
+ ? JSON.stringify([target.scope, target.anchorType, target.anchorId]) : ''
24
+
25
+ /** Domain-neutral session lifecycle. Never deletes history or edits host documents. */
26
+ export class AssistantSessionController {
27
+ state: AssistantSessionState = { sessions: [], sessionId: '', loading: false, error: '', revision: 0 }
28
+ private generation = 0
29
+ private preferred = new Map<string, string>()
30
+ private pending = new Map<string, Promise<AssistantSession[]>>()
31
+
32
+ private transport: AssistantSessionTransport
33
+ private changed: (state: AssistantSessionState) => void
34
+ private storage?: Pick<Storage, 'getItem' | 'setItem'>
35
+ private lazy: boolean
36
+ private preparing?: Promise<string | undefined>
37
+ constructor(transport: AssistantSessionTransport, changed: (state: AssistantSessionState) => void,
38
+ storage?: Pick<Storage, 'getItem' | 'setItem'>, options: { lazy?: boolean } = {}) {
39
+ this.transport = transport
40
+ this.changed = changed
41
+ this.storage = storage
42
+ this.lazy = !!options.lazy
43
+ }
44
+
45
+ private remember(key: string, id: string) {
46
+ this.preferred.set(key, id)
47
+ try { this.storage?.setItem('assistant-session:' + key, id) } catch { /* Storage is optional. */ }
48
+ }
49
+
50
+ private preference(key: string): string | undefined {
51
+ try { return this.preferred.get(key) || this.storage?.getItem('assistant-session:' + key) || undefined }
52
+ catch { return this.preferred.get(key) }
53
+ }
54
+
55
+ private publish(patch: Partial<AssistantSessionState>) {
56
+ this.state = { ...this.state, ...patch }
57
+ this.changed(this.state)
58
+ }
59
+
60
+ async open(target?: AssistantSessionTarget, force = false): Promise<void> {
61
+ const key = keyOf(target)
62
+ if (!force && key === keyOf(this.state.target) && (this.state.loading || this.state.sessionId || (this.lazy && this.state.target && !this.state.error))) return
63
+ const generation = ++this.generation
64
+ const snapshot = target ? { ...target } : undefined
65
+ this.publish({ target: snapshot, sessions: [], sessionId: '', loading: !!target, error: '' })
66
+ if (!snapshot) return
67
+ try {
68
+ let pending = this.pending.get(key)
69
+ if (!pending) {
70
+ pending = (async () => {
71
+ const sessions = await this.transport.list(snapshot)
72
+ // Failed/malformed reads must not create replacement sessions.
73
+ if (!Array.isArray(sessions) || sessions.some(session => !session.id)) throw new Error('Invalid session list')
74
+ if (sessions.length || this.lazy) return [...sessions]
75
+ // Do not issue a mutation for a target abandoned while the inbox was loading.
76
+ if (keyOf(this.state.target) !== key || this.pending.get(key) !== pending) throw new Error('Session target changed')
77
+ const created = await this.transport.create(snapshot)
78
+ if (!created?.id) throw new Error('Invalid new session')
79
+ return [created]
80
+ })()
81
+ this.pending.set(key, pending)
82
+ void pending.finally(() => {
83
+ if (this.pending.get(key) === pending) this.pending.delete(key)
84
+ }).catch(() => {})
85
+ }
86
+ const sessions = await pending
87
+ if (generation !== this.generation) return
88
+ const preference = this.preference(key)
89
+ const sessionId = this.lazy && preference === '__draft__' ? '' : sessions.find(session => session.id === preference)?.id || sessions[0]?.id || ''
90
+ this.remember(key, sessionId || '__draft__')
91
+ this.publish({ sessions, sessionId, loading: false })
92
+ } catch (error) {
93
+ if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })
94
+ }
95
+ }
96
+
97
+ select(id: string): boolean {
98
+ if (this.state.loading || !this.state.sessions.some(session => session.id === id)) return false
99
+ this.remember(keyOf(this.state.target), id)
100
+ this.publish({ sessionId: id, error: '' })
101
+ return true
102
+ }
103
+
104
+ async reset(): Promise<void> {
105
+ if (this.state.loading || this.preparing) return
106
+ if (this.lazy) {
107
+ if (!this.state.sessionId) return
108
+ ++this.generation
109
+ this.remember(keyOf(this.state.target), '__draft__')
110
+ this.publish({ sessionId: '', error: '', revision: this.state.revision + 1 })
111
+ return
112
+ }
113
+ const target = this.state.target
114
+ if (!target) {
115
+ this.publish({ sessions: [], sessionId: '', error: '', revision: this.state.revision + 1 })
116
+ return
117
+ }
118
+ const generation = ++this.generation
119
+ const key = keyOf(target)
120
+ this.publish({ loading: true, error: '' })
121
+ try {
122
+ if (this.state.sessionId && this.transport.isEmpty) {
123
+ const empty = await this.transport.isEmpty(this.state.sessionId)
124
+ if (generation !== this.generation) return
125
+ if (empty) { this.publish({ loading: false }); return }
126
+ }
127
+ const session = await this.transport.create({ ...target })
128
+ if (!session?.id) throw new Error('Invalid new session')
129
+ this.remember(key, session.id)
130
+ if (generation !== this.generation) return
131
+ this.publish({ sessions: [session, ...this.state.sessions.filter(existing => existing.id !== session.id)], sessionId: session.id,
132
+ loading: false, revision: this.state.revision + 1 })
133
+ } catch (error) {
134
+ if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })
135
+ }
136
+ }
137
+
138
+ /** Called only for a real send. Adoption happens before hosts render the new ID. */
139
+ prepareSession(adopt: (id: string) => void = () => {}): Promise<string | undefined> {
140
+ if (this.state.sessionId) return Promise.resolve(this.state.sessionId)
141
+ if (!this.state.target) return Promise.resolve(undefined)
142
+ if (this.state.loading || this.state.error) return Promise.reject(new Error('Session history is not ready'))
143
+ if (this.preparing) return this.preparing
144
+ const target = { ...this.state.target }, generation = this.generation
145
+ const pending = (async () => {
146
+ const session = await this.transport.create(target)
147
+ if (generation !== this.generation) throw new Error('Session target changed')
148
+ if (!session?.id) throw new Error('Invalid new session')
149
+ adopt(session.id)
150
+ this.remember(keyOf(target), session.id)
151
+ this.publish({ sessionId: session.id, sessions: [session, ...this.state.sessions] })
152
+ return session.id
153
+ })()
154
+ this.preparing = pending
155
+ void pending.finally(() => { if (this.preparing === pending) this.preparing = undefined }).catch(() => {})
156
+ return pending
157
+ }
158
+
159
+ /** Invalidate pending UI updates at logout/disposal. Server authorization remains authoritative. */
160
+ clear() {
161
+ ++this.generation
162
+ this.preferred.clear()
163
+ this.pending.clear()
164
+ this.publish({ target: undefined, sessions: [], sessionId: '', loading: false, error: '', revision: this.state.revision + 1 })
165
+ }
166
+ }
@@ -0,0 +1,34 @@
1
+ import { client } from '@operato/graphql'
2
+ import gql from 'graphql-tag'
3
+ import type { AssistantSessionTransport } from './assistant-session-controller'
4
+
5
+ /** Existing authorized session API; does not introduce a product-specific storage format. */
6
+ export const assistantSessionTransport: AssistantSessionTransport = {
7
+ async isEmpty(sessionId) {
8
+ const result = await client.query({
9
+ query: gql`query($id:String!){chatMessages(sessionId:$id,limit:1){id}}`,
10
+ variables: { id: sessionId }, fetchPolicy: 'no-cache'
11
+ })
12
+ if (result.errors?.length) throw new Error(result.errors[0].message)
13
+ if (!Array.isArray(result.data?.chatMessages)) throw new Error('대화 내용을 확인하지 못했습니다.')
14
+ return result.data.chatMessages.length === 0
15
+ },
16
+ async list(target) {
17
+ const result = await client.query({
18
+ query: gql`query($type:String!,$id:String!){chatSessionInbox(anchorType:$type,anchorId:$id,mineOnly:true,limit:100){id name}}`,
19
+ variables: { type: target.anchorType, id: target.anchorId }, fetchPolicy: 'no-cache'
20
+ })
21
+ if (result.errors?.length) throw new Error(result.errors[0].message)
22
+ if (!Array.isArray(result.data?.chatSessionInbox)) throw new Error('대화 목록을 불러오지 못했습니다.')
23
+ return result.data.chatSessionInbox
24
+ },
25
+ async create(target) {
26
+ const result = await client.mutate({
27
+ mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name}}`,
28
+ variables: { type: target.anchorType, id: target.anchorId }
29
+ })
30
+ if (result.errors?.length) throw new Error(result.errors[0].message)
31
+ if (!result.data?.createAISession?.id) throw new Error('대화를 만들지 못했습니다.')
32
+ return result.data.createAISession
33
+ }
34
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * <ox-board-ai-chat> — AI 주도 보드 모델링 채팅 컴포넌트 (Lit).
2
+ * <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
3
3
  *
4
4
  * 입력:
5
5
  * - sessionId: 영속 ChatSession 식별자 (없으면 ad-hoc 모드, 메시지 영속 안 됨)
@@ -14,12 +14,25 @@
14
14
  * 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
15
15
  */
16
16
  import '@material/web/icon/icon.js';
17
+ import './assistant-session-toolbar.js';
18
+ import { type AssistantRequestContext } from '../utils/assistant-request-context.js';
17
19
  import { LitElement } from 'lit';
18
20
  import './mention-popup.js';
19
21
  import { type FilterResult } from './mention-popup.js';
20
- export declare class OxBoardAIChat extends LitElement {
22
+ export interface ChatAttachment {
23
+ id: string;
24
+ file: File;
25
+ name: string;
26
+ size: number;
27
+ type: string;
28
+ url: string;
29
+ base64?: string;
30
+ }
31
+ export declare class OxAssistantChat extends LitElement {
21
32
  /** 영속 세션 id. 없으면 ad-hoc (메시지 영속 안 됨). */
22
33
  sessionId?: string;
34
+ prepareSession?: (adopt: (id: string) => void) => Promise<string | undefined>;
35
+ private adoptingSession;
23
36
  /**
24
37
  * 보드의 모든 ChatSession 리스트 — 다중 세션 활성화 시 탭 UI 로 표시.
25
38
  *
@@ -138,6 +151,8 @@ export declare class OxBoardAIChat extends LitElement {
138
151
  * 값이 들어오면 그 문장을 입력으로 삼아 한 번 전송하고 비운다(같은 질문이 반복 전송되지 않게).
139
152
  */
140
153
  autoAsk?: string;
154
+ autoAskRequestId?: string;
155
+ contextProvider?: () => AssistantRequestContext;
141
156
  /**
142
157
  * 빈 화면의 제목·부제 — **호스트가 자기 말로 준다.**
143
158
  *
@@ -176,6 +191,17 @@ export declare class OxBoardAIChat extends LitElement {
176
191
  private lastFailedInput?;
177
192
  /** 되돌려진 patch id 들 — 한 번 revert 한 patch 는 버튼 비활성 */
178
193
  private revertedPatchIds;
194
+ /** 첨부된 파일/이미지 목록 */
195
+ private attachments;
196
+ /** 드래그 오버 상태 (시각 피드백용) */
197
+ private dragOver;
198
+ private onAttachFiles;
199
+ private readFileAsBase64;
200
+ private removeAttachment;
201
+ private onComposerPaste;
202
+ private onComposerDragOver;
203
+ private onComposerDragLeave;
204
+ private onComposerDrop;
179
205
  /** mini action — 인라인 예시 토글 */
180
206
  private examplesOpen;
181
207
  /** 복사 toast 표시 상태 */
@@ -359,6 +385,7 @@ export declare class OxBoardAIChat extends LitElement {
359
385
  * 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
360
386
  */
361
387
  private renderProposals;
388
+ private onChoiceClick;
362
389
  /**
363
390
  * 제안 식별 키 — 같은 조치를 두 번 실행하지 않도록 **효과**(명령·대상·인자)로만 만든다.
364
391
  *
@@ -426,6 +453,6 @@ export declare class OxBoardAIChat extends LitElement {
426
453
  }
427
454
  declare global {
428
455
  interface HTMLElementTagNameMap {
429
- 'ox-board-ai-chat': OxBoardAIChat;
456
+ 'ox-assistant-chat': OxAssistantChat;
430
457
  }
431
458
  }