@things-factory/ai-assistant 10.1.79 → 10.1.81
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.
- package/client/components/assistant-chat.ts +102 -13
- package/client/components/assistant-dock.ts +160 -18
- package/client/components/assistant-mode.test.ts +16 -2
- package/client/components/assistant-session-manager.ts +52 -25
- package/client/components/assistant-session-toolbar.ts +4 -2
- package/client/components/chat-echo-dedup.ts +4 -0
- package/client/components/proposal-outcome-line.ts +36 -0
- package/client/components/proposal-request.test.ts +52 -0
- package/client/utils/assistant-navigation.test.ts +38 -0
- package/client/utils/assistant-navigation.ts +6 -0
- package/client/utils/assistant-surface.ts +21 -0
- package/dist-client/components/assistant-chat.d.ts +15 -0
- package/dist-client/components/assistant-chat.js +106 -13
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +13 -0
- package/dist-client/components/assistant-dock.js +175 -20
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-mode.test.js +14 -2
- package/dist-client/components/assistant-mode.test.js.map +1 -1
- package/dist-client/components/assistant-session-manager.d.ts +4 -0
- package/dist-client/components/assistant-session-manager.js +68 -26
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/assistant-session-toolbar.d.ts +1 -0
- package/dist-client/components/assistant-session-toolbar.js +8 -2
- package/dist-client/components/assistant-session-toolbar.js.map +1 -1
- package/dist-client/components/chat-echo-dedup.d.ts +2 -0
- package/dist-client/components/chat-echo-dedup.js +2 -0
- package/dist-client/components/chat-echo-dedup.js.map +1 -1
- package/dist-client/components/proposal-outcome-line.d.ts +29 -0
- package/dist-client/components/proposal-outcome-line.js +15 -0
- package/dist-client/components/proposal-outcome-line.js.map +1 -1
- package/dist-client/components/proposal-request.test.d.ts +1 -0
- package/dist-client/components/proposal-request.test.js +45 -0
- package/dist-client/components/proposal-request.test.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-navigation.d.ts +6 -0
- package/dist-client/utils/assistant-navigation.js +9 -0
- package/dist-client/utils/assistant-navigation.js.map +1 -0
- package/dist-client/utils/assistant-navigation.test.d.ts +1 -0
- package/dist-client/utils/assistant-navigation.test.js +37 -0
- package/dist-client/utils/assistant-navigation.test.js.map +1 -0
- package/dist-client/utils/assistant-surface.d.ts +19 -0
- package/dist-client/utils/assistant-surface.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.js +1 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/chat-message/chat-message.d.ts +10 -0
- package/dist-server/service/chat-message/chat-message.js +29 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -1
- package/dist-server/service/chat-message/proposal-outcome.d.ts +8 -0
- package/dist-server/service/chat-message/proposal-outcome.js +35 -1
- package/dist-server/service/chat-message/proposal-outcome.js.map +1 -1
- package/dist-server/service/chat-session/session-inbox.d.ts +7 -0
- package/dist-server/service/chat-session/session-inbox.js +7 -0
- package/dist-server/service/chat-session/session-inbox.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +1 -0
- package/dist-server/service/chat-session-resolver.js +63 -9
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/assistant-chat-resolver.ts +1 -0
- package/server/service/chat-message/chat-message.ts +32 -0
- package/server/service/chat-message/proposal-outcome.test.ts +53 -2
- package/server/service/chat-message/proposal-outcome.ts +33 -0
- package/server/service/chat-session/session-inbox.ts +7 -0
- package/server/service/chat-session-resolver.ts +71 -12
- package/test/assistant-dock.test.ts +8 -3
- package/test/assistant-session-toolbar.test.ts +7 -1
- package/test/open-doors.test.ts +2 -1
- package/test/session-discovery.test.ts +42 -0
- package/translations/en.json +26 -1
- package/translations/ja.json +26 -1
- package/translations/ko.json +26 -1
- package/translations/ms.json +26 -1
- package/translations/zh.json +26 -1
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
*/
|
|
62
62
|
import { chatDefaults } from './chat-defaults.js'
|
|
63
63
|
import { isDifferentConversation, resolveConversation, type AssistantMode, type AssistantModeView, pickInitialMode } from './assistant-mode.js'
|
|
64
|
-
import { parseProposalOutcomeLine, type ParsedProposalOutcome } from './proposal-outcome-line.js'
|
|
64
|
+
import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState, type ParsedProposalOutcome } from './proposal-outcome-line.js'
|
|
65
65
|
import { availableTriggers, type TriggerChip } from './chat-triggers.js'
|
|
66
66
|
|
|
67
67
|
interface ChatLine {
|
|
@@ -280,6 +280,7 @@ const CHAT_MESSAGES_QUERY = gql`
|
|
|
280
280
|
content
|
|
281
281
|
relatedPatchId
|
|
282
282
|
toolUsagesJson
|
|
283
|
+
proposalsJson
|
|
283
284
|
groundingWarningsJson
|
|
284
285
|
attachmentsJson
|
|
285
286
|
createdAt
|
|
@@ -302,6 +303,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql`
|
|
|
302
303
|
content
|
|
303
304
|
relatedPatchId
|
|
304
305
|
toolUsagesJson
|
|
306
|
+
proposalsJson
|
|
305
307
|
groundingWarningsJson
|
|
306
308
|
attachmentsJson
|
|
307
309
|
createdAt
|
|
@@ -802,6 +804,14 @@ export class OxAssistantChat extends LitElement {
|
|
|
802
804
|
@state()
|
|
803
805
|
private busy = false
|
|
804
806
|
|
|
807
|
+
/** Host navigation may retain a conversation for reading while its subject is off screen. */
|
|
808
|
+
@property() sendBlocked = ''
|
|
809
|
+
|
|
810
|
+
/** The host may inspect navigation risk without reading or copying the person's input. */
|
|
811
|
+
get navigationState(): { busy: boolean; draft: boolean } {
|
|
812
|
+
return { busy: this.busy, draft: !!this.input.trim() || this.attachments.length > 0 }
|
|
813
|
+
}
|
|
814
|
+
|
|
805
815
|
@state()
|
|
806
816
|
private errorMessage?: string
|
|
807
817
|
|
|
@@ -2900,6 +2910,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
2900
2910
|
const prevSameSender = !!prev && prev.role === 'user' && prev.senderEmail === line.senderEmail
|
|
2901
2911
|
const showSenderLabel = isOther && !prevSameSender
|
|
2902
2912
|
const content = (line.content ?? '').trim()
|
|
2913
|
+
/* A "requested" row is the card's state, shown on the card - as a line it would only be the machine's key. */
|
|
2914
|
+
if (line.role === 'system' && parseProposalRequestLine(content)) return nothing
|
|
2903
2915
|
/*
|
|
2904
2916
|
* 가려진 줄은 어시스턴트의 말이 아니다 — 마크다운으로 그리지 않고, 복사·재실행 같은 손잡이도
|
|
2905
2917
|
* 달지 않는다(ADR-0073 판정 1). 그 손잡이들은 있지도 않은 답을 다룬다.
|
|
@@ -3145,9 +3157,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
3145
3157
|
`
|
|
3146
3158
|
: nothing}
|
|
3147
3159
|
<button
|
|
3148
|
-
?disabled=${this.busy || (!this.input.trim() && this.attachments.length === 0)}
|
|
3160
|
+
?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
|
|
3149
3161
|
@click=${this.send}
|
|
3150
|
-
title=${i18next.t('ai-assistant.text.send-tooltip')}>
|
|
3162
|
+
title=${this.sendBlocked || i18next.t('ai-assistant.text.send-tooltip')}>
|
|
3151
3163
|
${this.busy
|
|
3152
3164
|
? html`<span class="spinner"></span>`
|
|
3153
3165
|
: html`<md-icon>arrow_upward</md-icon>`}
|
|
@@ -3930,11 +3942,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
3930
3942
|
private renderProposals(line: ChatLine) {
|
|
3931
3943
|
const items = line.proposals ?? []
|
|
3932
3944
|
if (items.length === 0) return nothing
|
|
3945
|
+
/* The card's state is the conversation's rows - read once for every card of this line. */
|
|
3946
|
+
const outcomes = this.proposalOutcomes
|
|
3947
|
+
const requested = this.proposalRequests
|
|
3933
3948
|
return html`
|
|
3934
3949
|
<div class="proposals">
|
|
3935
3950
|
${items.map(p => {
|
|
3936
3951
|
const key = this.proposalKey(p)
|
|
3937
|
-
const
|
|
3952
|
+
const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) })
|
|
3953
|
+
const sent = card.state !== 'ready'
|
|
3938
3954
|
/*
|
|
3939
3955
|
Follow-up chips are the host's words, never this component's. The fallback that used
|
|
3940
3956
|
to live here named a 3D modeller's operations in hardcoded Korean, and it reached the
|
|
@@ -3956,14 +3972,23 @@ export class OxAssistantChat extends LitElement {
|
|
|
3956
3972
|
the one action the card exists for became unreachable while the model kept
|
|
3957
3973
|
telling people to press it.
|
|
3958
3974
|
*/ ''}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3975
|
+
${card.state === 'unconfirmed'
|
|
3976
|
+
? html`
|
|
3977
|
+
<span class="preason">${i18next.t('ai-assistant.text.proposal-unconfirmed', { defaultValue: 'sent earlier - no answer received' })}</span>
|
|
3978
|
+
<button class="run" @click=${() => this.checkProposal(p, key)}>
|
|
3979
|
+
${i18next.t('ai-assistant.button.check-proposal', { defaultValue: 'check result' })}
|
|
3980
|
+
</button>
|
|
3981
|
+
`
|
|
3982
|
+
: html`
|
|
3983
|
+
<button
|
|
3984
|
+
class="run"
|
|
3985
|
+
?disabled=${sent}
|
|
3986
|
+
@click=${() => this.executeProposal(p, key)}>
|
|
3987
|
+
${sent
|
|
3988
|
+
? this.proposalStateText(key)
|
|
3989
|
+
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
|
|
3990
|
+
</button>
|
|
3991
|
+
`}
|
|
3967
3992
|
${/*
|
|
3968
3993
|
Drawn only where the host has said it staged this candidate, in the host's own
|
|
3969
3994
|
words. Not knowing is not a claim: with no word from the host, nothing is drawn.
|
|
@@ -4120,10 +4145,34 @@ export class OxAssistantChat extends LitElement {
|
|
|
4120
4145
|
|
|
4121
4146
|
private elapsedTimer?: ReturnType<typeof setInterval>
|
|
4122
4147
|
|
|
4123
|
-
private executeProposal(p: any, key: string) {
|
|
4148
|
+
private async executeProposal(p: any, key: string) {
|
|
4149
|
+
if (this.sentProposals.has(key)) return
|
|
4124
4150
|
this.sentProposals = new Set(this.sentProposals).add(key)
|
|
4125
4151
|
this.proposalSentAt.set(key, Date.now())
|
|
4126
4152
|
this._startElapsedTicker()
|
|
4153
|
+
|
|
4154
|
+
/*
|
|
4155
|
+
* The server records the request first and answers whether this conversation has taken the proposal
|
|
4156
|
+
* already (ADR-0097 Q-D4 ④). Only a granted request reaches the host. A refused one - taken in another
|
|
4157
|
+
* window, or before a refresh - leaves the card to the rows; a failed one leaves it as it was, and the
|
|
4158
|
+
* error is said by the client's own error line. Either way the host is not asked.
|
|
4159
|
+
*/
|
|
4160
|
+
if (this.sessionId) {
|
|
4161
|
+
let granted = false
|
|
4162
|
+
try {
|
|
4163
|
+
granted = await this.requestProposal(key)
|
|
4164
|
+
} catch {
|
|
4165
|
+
granted = false
|
|
4166
|
+
}
|
|
4167
|
+
if (!granted) {
|
|
4168
|
+
const left = new Set(this.sentProposals)
|
|
4169
|
+
left.delete(key)
|
|
4170
|
+
this.sentProposals = left
|
|
4171
|
+
this.proposalSentAt.delete(key)
|
|
4172
|
+
return
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4127
4176
|
this.dispatchEvent(
|
|
4128
4177
|
new CustomEvent('assistant-proposal-execute', {
|
|
4129
4178
|
/*
|
|
@@ -4137,6 +4186,44 @@ export class OxAssistantChat extends LitElement {
|
|
|
4137
4186
|
)
|
|
4138
4187
|
}
|
|
4139
4188
|
|
|
4189
|
+
private async requestProposal(key: string): Promise<boolean> {
|
|
4190
|
+
const result = await client.mutate({
|
|
4191
|
+
mutation: gql`
|
|
4192
|
+
mutation ($id: String!, $key: String!) {
|
|
4193
|
+
requestProposal(sessionId: $id, proposalKey: $key)
|
|
4194
|
+
}
|
|
4195
|
+
`,
|
|
4196
|
+
variables: { id: this.sessionId, key }
|
|
4197
|
+
})
|
|
4198
|
+
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
4199
|
+
return result.data?.requestProposal === true
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
/**
|
|
4203
|
+
* "Check result" on a card that was requested and never answered: the host is asked what happened
|
|
4204
|
+
* (`AssistantSurface.proposalOutcome`), never to run it again. The dock reports what it learns.
|
|
4205
|
+
*/
|
|
4206
|
+
private checkProposal(p: any, key: string) {
|
|
4207
|
+
this.dispatchEvent(
|
|
4208
|
+
new CustomEvent('assistant-proposal-check', {
|
|
4209
|
+
detail: { proposal: p, sessionId: this.sessionId, proposalKey: key },
|
|
4210
|
+
bubbles: true,
|
|
4211
|
+
composed: true
|
|
4212
|
+
})
|
|
4213
|
+
)
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
/** Keys whose run was requested - from the conversation's rows, like the outcomes. */
|
|
4217
|
+
private get proposalRequests(): Set<string> {
|
|
4218
|
+
const requested = new Set<string>()
|
|
4219
|
+
for (const line of this.lines) {
|
|
4220
|
+
if (line.role !== 'system') continue
|
|
4221
|
+
const key = parseProposalRequestLine(line.content ?? '')
|
|
4222
|
+
if (key) requested.add(key)
|
|
4223
|
+
}
|
|
4224
|
+
return requested
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4140
4227
|
/**
|
|
4141
4228
|
* 호스트가 무엇을 했는지 — 대화에 남은 `system` 행에서 읽는다(ADR-0070 A12-a 결정 ①).
|
|
4142
4229
|
*
|
|
@@ -4447,6 +4534,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
4447
4534
|
}
|
|
4448
4535
|
|
|
4449
4536
|
private async send() {
|
|
4537
|
+
if (this.sendBlocked) { this.errorMessage = this.sendBlocked; return }
|
|
4450
4538
|
const text = this.input.trim()
|
|
4451
4539
|
const attachmentsToSend = [...this.attachments]
|
|
4452
4540
|
if ((!text && attachmentsToSend.length === 0) || this.busy) return
|
|
@@ -4508,6 +4596,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
4508
4596
|
modelFingerprint: assistantModelFingerprint(liveBoard) }
|
|
4509
4597
|
} catch (error) {
|
|
4510
4598
|
this.errorMessage = error instanceof Error ? error.message : String(error)
|
|
4599
|
+
this.attachments = attachmentsToSend
|
|
4511
4600
|
return
|
|
4512
4601
|
}
|
|
4513
4602
|
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '../utils/assistant-surface.js'
|
|
32
32
|
import { runProposal, type ProposalContext, type ProposalOutcome } from '../utils/assistant-proposals.js'
|
|
33
33
|
import { holdAssistantAsk, subscribeAssistantAsk, takeAssistantAsk, type AssistantAsk } from '../utils/assistant-ask.js'
|
|
34
|
+
import { surfaceTransition } from '../utils/assistant-navigation.js'
|
|
34
35
|
|
|
35
36
|
export const ASSISTANT_DOCK_VIEWPART = 'assistant-dock'
|
|
36
37
|
|
|
@@ -191,6 +192,35 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
191
192
|
gap: 4px;
|
|
192
193
|
font: inherit;
|
|
193
194
|
}
|
|
195
|
+
|
|
196
|
+
.next-step {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: space-between;
|
|
200
|
+
gap: 10px;
|
|
201
|
+
padding: 8px 12px;
|
|
202
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
203
|
+
background: var(--md-sys-color-surface-container-low);
|
|
204
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
205
|
+
font-size: 12px;
|
|
206
|
+
line-height: 1.4;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.next-step a {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 3px;
|
|
213
|
+
flex: none;
|
|
214
|
+
color: var(--md-sys-color-primary);
|
|
215
|
+
font-weight: 650;
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.next-step a:hover { text-decoration: underline; }
|
|
220
|
+
.next-step md-icon { --md-icon-size: 16px; }
|
|
221
|
+
ox-assistant-chat[hidden] { display: none; }
|
|
222
|
+
.transition { padding: 10px 12px; font-size: 13px; line-height: 1.5; border-bottom: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-low); }
|
|
223
|
+
.transition button { display: block; margin-top: 6px; font: inherit; }
|
|
194
224
|
`
|
|
195
225
|
]
|
|
196
226
|
|
|
@@ -211,6 +241,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
211
241
|
@state() private showingHistory = false
|
|
212
242
|
@state() private conversationView = 0
|
|
213
243
|
@state() private asking?: { ask?: string; draft?: string; requestId: string }
|
|
244
|
+
@state() private screenChanged = false
|
|
245
|
+
@state() private navigationNotice = ''
|
|
246
|
+
private openedIdentity = ''
|
|
214
247
|
|
|
215
248
|
private app = new AppController(this)
|
|
216
249
|
/** The kind the person picked; kept while the surface stays the same. */
|
|
@@ -242,6 +275,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
242
275
|
this.addEventListener('assistant-start-new-session', this.onStartNewSession)
|
|
243
276
|
this.unsubscribers.push(
|
|
244
277
|
subscribeAssistantSurface(() => this.follow()),
|
|
278
|
+
store.subscribe(() => { if (this.openedIdentity !== this.identityKey()) this.follow() }),
|
|
245
279
|
subscribeAssistantAsk(() => void this.takeAsk()),
|
|
246
280
|
subscribeSessionActivity(activity => {
|
|
247
281
|
/* A rename or a new conversation elsewhere on this anchor - the tabs read again. */
|
|
@@ -260,24 +294,75 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
260
294
|
super.disconnectedCallback()
|
|
261
295
|
}
|
|
262
296
|
|
|
263
|
-
private scopeKey(surface
|
|
297
|
+
private scopeKey(surface: AssistantSurface | undefined) {
|
|
264
298
|
return JSON.stringify([this.app.contextPath, surface?.key ?? ''])
|
|
265
299
|
}
|
|
266
300
|
|
|
301
|
+
private identityKey() {
|
|
302
|
+
const user = (store.getState() as any).auth?.user
|
|
303
|
+
return JSON.stringify([this.app.contextPath, user?.id || user?.email || ''])
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private get navigationState() {
|
|
307
|
+
return (this.renderRoot.querySelector('ox-assistant-chat') as any)?.navigationState ?? { busy: false, draft: false }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async mayLeaveConversation(): Promise<boolean> {
|
|
311
|
+
if (this.navigationState.busy) {
|
|
312
|
+
notify({ level: 'info', message: t('wait-before-switch', 'Wait for the current response before changing conversations.') })
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
if (!this.navigationState.draft) return true
|
|
316
|
+
const identity = this.identityKey()
|
|
317
|
+
const accepted = await OxPrompt.open({
|
|
318
|
+
type: 'warning', title: t('switch-title', 'Change conversation?'),
|
|
319
|
+
text: t('switch-draft', 'Your unsent text and attachments will not move to the other conversation.'),
|
|
320
|
+
confirmButton: { text: t('switch-confirm', 'Discard draft and switch') },
|
|
321
|
+
cancelButton: { text: t('stay', 'Keep writing') }
|
|
322
|
+
})
|
|
323
|
+
return !!accepted && identity === this.identityKey()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async requestSurface(surface: AssistantSurface | undefined) {
|
|
327
|
+
if (!(await this.mayLeaveConversation())) return
|
|
328
|
+
this.chosenKind = ''
|
|
329
|
+
this.surface = surface
|
|
330
|
+
this.navigationNotice = surface ? t('now-about', 'Conversation subject: {name}', { name: surface.label() }) : ''
|
|
331
|
+
await this.open()
|
|
332
|
+
}
|
|
333
|
+
|
|
267
334
|
/** The screen changed - a different surface or domain is a different conversation. */
|
|
268
335
|
private follow(force = false) {
|
|
269
336
|
const next = currentAssistantSurface()
|
|
270
|
-
|
|
271
|
-
|
|
337
|
+
const transition = surfaceTransition({
|
|
338
|
+
identityChanged: this.openedIdentity !== this.identityKey(),
|
|
339
|
+
sameSubject: this.scopeKey(next) === this.openedFor,
|
|
340
|
+
active: isAnchor(this.anchor) && (!!this.sessionId || this.navigationState.draft || this.navigationState.busy)
|
|
341
|
+
})
|
|
342
|
+
if (!force && transition === 'refresh') {
|
|
343
|
+
this.surface = next
|
|
344
|
+
this.screenChanged = false
|
|
272
345
|
this.requestUpdate()
|
|
273
346
|
return
|
|
274
347
|
}
|
|
348
|
+
if (!force && transition === 'offer') {
|
|
349
|
+
this.screenChanged = true
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
if (transition === 'reset') {
|
|
353
|
+
this.asking = undefined
|
|
354
|
+
this.capability = 'unknown'
|
|
355
|
+
if (this.openedIdentity) takeAssistantAsk()
|
|
356
|
+
}
|
|
357
|
+
this.navigationNotice = transition === 'follow' && next ? t('now-about', 'Conversation subject: {name}', { name: next.label() }) : ''
|
|
275
358
|
if (next?.key !== this.surface?.key) this.chosenKind = ''
|
|
276
359
|
this.surface = next
|
|
277
360
|
void this.open()
|
|
278
361
|
}
|
|
279
362
|
|
|
280
363
|
private async open(): Promise<void> {
|
|
364
|
+
this.openedIdentity = this.identityKey()
|
|
365
|
+
this.screenChanged = false
|
|
281
366
|
const ticket = ++this.revision
|
|
282
367
|
this.conversationView++
|
|
283
368
|
this.showingHistory = false
|
|
@@ -285,7 +370,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
285
370
|
this.loading = true
|
|
286
371
|
this.failure = ''
|
|
287
372
|
this.anchor = undefined
|
|
288
|
-
this.openedFor = this.scopeKey()
|
|
373
|
+
this.openedFor = this.scopeKey(this.surface)
|
|
289
374
|
|
|
290
375
|
try {
|
|
291
376
|
if (this.capability !== 'configured') {
|
|
@@ -341,13 +426,15 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
341
426
|
|
|
342
427
|
private async newConversation() {
|
|
343
428
|
if (this.loading || !isAnchor(this.anchor)) return
|
|
429
|
+
if (!(await this.mayLeaveConversation())) return
|
|
344
430
|
this.showingHistory = false
|
|
345
431
|
const revision = this.controller.state.revision
|
|
346
432
|
await this.controller.reset()
|
|
347
433
|
if (revision !== this.controller.state.revision) this.conversationView++
|
|
348
434
|
}
|
|
349
435
|
|
|
350
|
-
private selectSession(id: string, pickedKind?: string) {
|
|
436
|
+
private async selectSession(id: string, pickedKind?: string) {
|
|
437
|
+
if (id !== this.sessionId && !(await this.mayLeaveConversation())) return
|
|
351
438
|
this.showingHistory = false
|
|
352
439
|
const follow = followPickedConversation(pickedKind, this.kind)
|
|
353
440
|
if (follow) {
|
|
@@ -360,9 +447,10 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
360
447
|
if (this.controller.select(id) && previous !== id) this.conversationView++
|
|
361
448
|
}
|
|
362
449
|
|
|
363
|
-
private onModeChange = (event: CustomEvent) => {
|
|
450
|
+
private onModeChange = async (event: CustomEvent) => {
|
|
364
451
|
const kind = event.detail?.kind
|
|
365
452
|
if (!kind || kind === this.kind) return
|
|
453
|
+
if (!(await this.mayLeaveConversation())) return
|
|
366
454
|
this.chosenKind = kind
|
|
367
455
|
void this.open()
|
|
368
456
|
}
|
|
@@ -371,6 +459,11 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
371
459
|
|
|
372
460
|
private async takeAsk() {
|
|
373
461
|
if (this.loading || !this.anchor) return
|
|
462
|
+
if (this.screenChanged || this.navigationState.busy || this.navigationState.draft) {
|
|
463
|
+
const waiting = takeAssistantAsk()
|
|
464
|
+
if (waiting) notify({ level: 'info', message: t('ask-kept-current', 'The current conversation was kept. Switch to the intended subject before asking again.') })
|
|
465
|
+
return
|
|
466
|
+
}
|
|
374
467
|
if (!isAnchor(this.anchor)) {
|
|
375
468
|
/*
|
|
376
469
|
A question is not sent later on its own, once some conversation happens to stand - by then the person is looking
|
|
@@ -442,6 +535,8 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
442
535
|
/* ── proposals ───────────────────────────────────────────────────────────────────── */
|
|
443
536
|
|
|
444
537
|
private proposalContext(sessionId?: string): ProposalContext {
|
|
538
|
+
const identity = this.identityKey()
|
|
539
|
+
const scope = this.openedFor
|
|
445
540
|
return {
|
|
446
541
|
sessionId,
|
|
447
542
|
surface: this.surface,
|
|
@@ -452,7 +547,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
452
547
|
text,
|
|
453
548
|
confirmButton: { text: confirmText ?? t('confirm', 'OK') },
|
|
454
549
|
cancelButton: { text: cancelText ?? t('cancel', 'Cancel') }
|
|
455
|
-
})),
|
|
550
|
+
})) && identity === this.identityKey() && scope === this.scopeKey(currentAssistantSurface()),
|
|
456
551
|
notify: (level, message) => notify({ level, message })
|
|
457
552
|
}
|
|
458
553
|
}
|
|
@@ -476,7 +571,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
476
571
|
/** Every proposal ends in exactly one reported outcome - a kind nobody handles is `unavailable`. */
|
|
477
572
|
private onProposal = async (event: CustomEvent) => {
|
|
478
573
|
const { proposal, sessionId, proposalKey } = event.detail ?? {}
|
|
479
|
-
const outcome =
|
|
574
|
+
const outcome: ProposalOutcome = this.openedFor !== this.scopeKey(currentAssistantSurface()) || this.openedIdentity !== this.identityKey()
|
|
575
|
+
? { verdict: 'unavailable', errorCode: 'conversation-screen-changed', note: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') }
|
|
576
|
+
: await runProposal(proposal, this.proposalContext(sessionId))
|
|
480
577
|
if (outcome.verdict === 'unavailable' && outcome.errorCode === 'unknown-proposal-kind') {
|
|
481
578
|
notify({ level: 'error', message: t('proposal-not-runnable', 'this proposal cannot be run on this screen.') })
|
|
482
579
|
}
|
|
@@ -485,6 +582,33 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
485
582
|
if (outcome.note && sessionId) await this.appendNote(sessionId, outcome.note)
|
|
486
583
|
}
|
|
487
584
|
|
|
585
|
+
/**
|
|
586
|
+
* "Check result" - asks the surface what became of a requested proposal and reports only what it learns.
|
|
587
|
+
* Nothing is run: a lost answer never turns into a second application (ADR-0097 Q-D4 ④).
|
|
588
|
+
*/
|
|
589
|
+
private onProposalCheck = async (event: CustomEvent) => {
|
|
590
|
+
const { proposal, sessionId, proposalKey } = event.detail ?? {}
|
|
591
|
+
const ask = this.surface?.proposalOutcome
|
|
592
|
+
if (!ask) {
|
|
593
|
+
notify({ level: 'info', message: t('proposal-result-uncheckable', 'this screen cannot check that result. open the screen where it was applied.') })
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let outcome: ProposalOutcome | undefined
|
|
598
|
+
try {
|
|
599
|
+
outcome = await ask.call(this.surface, proposalKey, proposal)
|
|
600
|
+
} catch {
|
|
601
|
+
outcome = undefined
|
|
602
|
+
}
|
|
603
|
+
if (!outcome?.verdict) {
|
|
604
|
+
notify({ level: 'info', message: t('proposal-result-unknown', 'the result is still unknown. it was not applied again.') })
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
await this.report(sessionId, proposalKey, outcome)
|
|
609
|
+
if (outcome.note && sessionId) await this.appendNote(sessionId, outcome.note)
|
|
610
|
+
}
|
|
611
|
+
|
|
488
612
|
private async appendNote(sessionId: string, note: string) {
|
|
489
613
|
try {
|
|
490
614
|
await client.mutate({
|
|
@@ -503,15 +627,22 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
503
627
|
/* ── board surfaces: the chat's scene edits land on the open board ───────────────── */
|
|
504
628
|
|
|
505
629
|
private onScenePatch = async (event: CustomEvent) => {
|
|
630
|
+
if (!this.mayApplyOnScreen()) return
|
|
506
631
|
const board = this.surface?.board
|
|
507
632
|
if (!board) return
|
|
508
633
|
const result = await board.applyPatch(event.detail)
|
|
509
634
|
if (result?.missed) notify({ level: 'info', message: t('patch-missed', '{missed} of the changes could not be applied.', { missed: result.missed }) })
|
|
510
635
|
}
|
|
511
636
|
|
|
512
|
-
private
|
|
637
|
+
private mayApplyOnScreen() {
|
|
638
|
+
if (this.openedFor === this.scopeKey(currentAssistantSurface()) && this.openedIdentity === this.identityKey()) return true
|
|
639
|
+
notify({ level: 'info', message: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') })
|
|
640
|
+
return false
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private onBoardRevert = (event: CustomEvent) => { if (this.mayApplyOnScreen()) void this.surface?.board?.revertPatch?.(event.detail?.patchId) }
|
|
513
644
|
|
|
514
|
-
private onSceneAction = (event: CustomEvent) => void this.surface?.board?.applyAction?.(event.detail)
|
|
645
|
+
private onSceneAction = (event: CustomEvent) => { if (this.mayApplyOnScreen()) void this.surface?.board?.applyAction?.(event.detail) }
|
|
515
646
|
|
|
516
647
|
/* ── drawing ─────────────────────────────────────────────────────────────────────── */
|
|
517
648
|
|
|
@@ -540,16 +671,16 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
540
671
|
<md-icon>home</md-icon>
|
|
541
672
|
</button>`
|
|
542
673
|
: nothing}
|
|
543
|
-
${ready
|
|
674
|
+
${ready && this.showingHistory
|
|
544
675
|
? html`<button
|
|
545
676
|
class="sbtn"
|
|
546
677
|
title=${this.showingHistory ? t('back-to-conversation', 'back to the conversation') : t('conversations', 'conversations')}
|
|
547
678
|
@click=${() => (this.showingHistory = !this.showingHistory)}
|
|
548
679
|
>
|
|
549
|
-
<md-icon>${
|
|
680
|
+
<md-icon>arrow_back</md-icon>${t('back-to-conversation', 'back to the conversation')}
|
|
550
681
|
</button>`
|
|
551
682
|
: nothing}
|
|
552
|
-
<button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.
|
|
683
|
+
<button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.requestSurface(currentAssistantSurface())}>
|
|
553
684
|
<md-icon>refresh</md-icon>
|
|
554
685
|
</button>
|
|
555
686
|
<button class="sbtn" title=${t('close', 'close')} @click=${() => closeOverlay(this.viewpart)}>
|
|
@@ -561,9 +692,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
561
692
|
|
|
562
693
|
/** The home button: talk about the application instead of this screen, until the screen changes. */
|
|
563
694
|
private useSurface(surface: AssistantSurface) {
|
|
564
|
-
this.surface
|
|
565
|
-
this.chosenKind = ''
|
|
566
|
-
void this.open()
|
|
695
|
+
void this.requestSurface(surface)
|
|
567
696
|
}
|
|
568
697
|
|
|
569
698
|
render(): TemplateResult {
|
|
@@ -578,6 +707,14 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
578
707
|
|
|
579
708
|
return html`
|
|
580
709
|
${this.header()}
|
|
710
|
+
${surface?.nextStep ? html`<div class="next-step" role="status">
|
|
711
|
+
<span>${surface.nextStep.description?.() || ''}</span>
|
|
712
|
+
<a href=${surface.nextStep.href}>${surface.nextStep.label()}<md-icon>arrow_forward</md-icon></a>
|
|
713
|
+
</div>` : nothing}
|
|
714
|
+
${this.screenChanged ? html`<div class="transition" role="status">
|
|
715
|
+
${t('screen-and-conversation', 'Screen: {screen}. Conversation: {conversation}. Your conversation has been kept.', { screen: currentAssistantSurface()?.label() || t('no-subject', 'No subject'), conversation: surface?.label() || '' })}
|
|
716
|
+
<button class="sbtn" @click=${() => this.requestSurface(currentAssistantSurface())}>${t('switch-to-screen', 'Talk about this screen')}</button>
|
|
717
|
+
</div>` : this.navigationNotice ? html`<div class="transition" role="status">${this.navigationNotice}</div>` : nothing}
|
|
581
718
|
${ready && this.showingHistory
|
|
582
719
|
? html`<ox-assistant-session-manager
|
|
583
720
|
.anchorType=${(anchor as any).anchorType}
|
|
@@ -585,6 +722,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
585
722
|
.sessionId=${this.sessionId}
|
|
586
723
|
.kind=${this.kind}
|
|
587
724
|
.kindLabels=${this.kindLabels}
|
|
725
|
+
.scopeLabel=${t('conversation-scope', 'Conversations · {name}', { name: surface?.label() || '' })}
|
|
588
726
|
@assistant-session-select=${(event: CustomEvent) => this.selectSession(event.detail.sessionId, event.detail.kind)}
|
|
589
727
|
@assistant-session-reset=${() => this.newConversation()}
|
|
590
728
|
@assistant-session-changed=${() => this.reloadSessions()}
|
|
@@ -597,21 +735,24 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
597
735
|
.loading=${this.loading}
|
|
598
736
|
.error=${this.failure}
|
|
599
737
|
historyEnabled
|
|
738
|
+
singleConversation
|
|
600
739
|
@assistant-session-select=${(event: CustomEvent) => this.selectSession(event.detail.sessionId)}
|
|
601
740
|
@assistant-session-reset=${() => this.newConversation()}
|
|
602
741
|
@assistant-session-history=${() => (this.showingHistory = true)}
|
|
603
742
|
@assistant-session-retry=${() => this.open()}
|
|
604
743
|
></ox-assistant-session-toolbar>`
|
|
605
744
|
: nothing}
|
|
606
|
-
${ready && !this.
|
|
745
|
+
${ready && !this.loading && !this.failure
|
|
607
746
|
? keyed(
|
|
608
747
|
this.conversationView,
|
|
609
748
|
html`<ox-assistant-chat
|
|
749
|
+
?hidden=${this.showingHistory}
|
|
750
|
+
.sendBlocked=${this.screenChanged ? t('return-to-subject', 'Return to the conversation subject before sending or applying changes.') : ''}
|
|
610
751
|
.sessionId=${this.sessionId}
|
|
611
752
|
.prepareSession=${(adopt: (id: string) => void) => this.controller.prepareSession(adopt)}
|
|
612
753
|
.contextProvider=${() => {
|
|
613
754
|
/* The screen moved on between writing and sending - do not answer about the old one. */
|
|
614
|
-
if (openedFor !== this.scopeKey(currentAssistantSurface()
|
|
755
|
+
if (openedFor !== this.scopeKey(currentAssistantSurface()) || this.openedIdentity !== this.identityKey()) throw new Error(t('return-to-subject', 'Return to the conversation subject before applying this proposal.'))
|
|
615
756
|
return { hostContext: surface?.hostContext?.() ?? {} }
|
|
616
757
|
}}
|
|
617
758
|
.mode=${mode}
|
|
@@ -631,6 +772,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
631
772
|
.draftRequestId=${this.asking?.requestId}
|
|
632
773
|
@assistant-mode-change=${this.onModeChange}
|
|
633
774
|
@assistant-proposal-execute=${this.onProposal}
|
|
775
|
+
@assistant-proposal-check=${this.onProposalCheck}
|
|
634
776
|
@scene-edit-patch=${this.onScenePatch}
|
|
635
777
|
@board-edit-revert=${this.onBoardRevert}
|
|
636
778
|
@scene-action-execute=${this.onSceneAction}
|
|
@@ -24,9 +24,13 @@ const MODE_VIEW = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** The board product, which declares no mode and names its own door. */
|
|
27
|
+
/*
|
|
28
|
+
* A host on the neutral door. A board host (`chatEndpoint: 'boardAIChat'`) keeps its own door even with
|
|
29
|
+
* a mode - it alone carries the board document (16d14bce2b, ADR-0061 decision 0-1) - and is pinned below.
|
|
30
|
+
*/
|
|
27
31
|
const HOST_PROPERTIES = {
|
|
28
32
|
hostContext: { boardId: 'B-1' },
|
|
29
|
-
chatEndpoint: '
|
|
33
|
+
chatEndpoint: 'assistantChat' as const
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const GLOBAL_DEFAULTS = {
|
|
@@ -70,7 +74,7 @@ describe('a mode names the conversation; the server says what it offers', () =>
|
|
|
70
74
|
|
|
71
75
|
describe('a host with no mode', () => {
|
|
72
76
|
test('gets its own properties and the registered defaults, as before modes existed', () => {
|
|
73
|
-
const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
77
|
+
const c = resolveConversation(undefined, { ...HOST_PROPERTIES, chatEndpoint: 'boardAIChat' }, GLOBAL_DEFAULTS)
|
|
74
78
|
|
|
75
79
|
expect(c.hostContext).toEqual({ boardId: 'B-1' })
|
|
76
80
|
expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)
|
|
@@ -145,3 +149,13 @@ describe('followPickedConversation', () => {
|
|
|
145
149
|
expect(followPickedConversation('', 'figure.modeller')).toBeUndefined()
|
|
146
150
|
})
|
|
147
151
|
})
|
|
152
|
+
|
|
153
|
+
describe('a board host keeps the board door', () => {
|
|
154
|
+
test('★ with a mode, the door stays boardAIChat and the board defaults fill what the mode leaves out', () => {
|
|
155
|
+
const c = resolveConversation(MODE, { ...HOST_PROPERTIES, chatEndpoint: 'boardAIChat' }, GLOBAL_DEFAULTS, undefined)
|
|
156
|
+
|
|
157
|
+
expect(c.kind).toBe('twin.space')
|
|
158
|
+
expect(c.chatEndpoint).toBe('boardAIChat')
|
|
159
|
+
expect(c.catalogEntries).toEqual(GLOBAL_DEFAULTS.catalogEntries)
|
|
160
|
+
})
|
|
161
|
+
})
|