@things-factory/ai-assistant 10.1.79 → 10.1.87
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 +206 -18
- package/client/components/assistant-mode.test.ts +16 -2
- package/client/components/assistant-session-manager.ts +58 -26
- 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-session-controller.ts +2 -0
- package/client/utils/assistant-session-transport.ts +2 -2
- 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 +16 -0
- package/dist-client/components/assistant-dock.js +221 -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 +74 -27
- 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-session-controller.d.ts +2 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-transport.js +2 -2
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- 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/chat-session.d.ts +5 -5
- package/dist-server/service/chat-session/chat-session.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/session-visibility.d.ts +8 -5
- package/dist-server/service/chat-session/session-visibility.js +9 -15
- package/dist-server/service/chat-session/session-visibility.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +1 -0
- package/dist-server/service/chat-session-resolver.js +64 -10
- 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/chat-session.ts +5 -5
- package/server/service/chat-session/session-inbox.ts +7 -0
- package/server/service/chat-session/session-visibility.test.ts +6 -6
- package/server/service/chat-session/session-visibility.ts +9 -15
- package/server/service/chat-session-resolver.ts +72 -13
- package/server/service/session-anchor.test.ts +2 -2
- 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 +28 -2
- package/translations/ja.json +28 -2
- package/translations/ko.json +28 -2
- package/translations/ms.json +28 -2
- package/translations/zh.json +28 -2
|
@@ -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
|
|
|
@@ -159,6 +160,34 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
159
160
|
--md-icon-size: 18px;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
/*
|
|
164
|
+
* An open conversation is read by everyone in the domain - the dock says so on every screen of it, not
|
|
165
|
+
* only in the list (2026-09-30: open and private were one grey word apart, and an owner's question about
|
|
166
|
+
* the books sat in every employee's list). A private one draws nothing extra: it is the default.
|
|
167
|
+
*/
|
|
168
|
+
:host([audience='open']) {
|
|
169
|
+
border-left: 3px solid var(--md-sys-color-tertiary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.audience {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 6px;
|
|
176
|
+
flex: none;
|
|
177
|
+
padding: 6px 10px;
|
|
178
|
+
background: var(--md-sys-color-tertiary-container);
|
|
179
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
180
|
+
border-bottom: 1px solid var(--md-sys-color-tertiary);
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
line-height: 1.35;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.audience md-icon {
|
|
187
|
+
--md-icon-size: 16px;
|
|
188
|
+
flex: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
ox-assistant-session-toolbar {
|
|
163
192
|
flex: none;
|
|
164
193
|
min-width: 0;
|
|
@@ -191,6 +220,35 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
191
220
|
gap: 4px;
|
|
192
221
|
font: inherit;
|
|
193
222
|
}
|
|
223
|
+
|
|
224
|
+
.next-step {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: space-between;
|
|
228
|
+
gap: 10px;
|
|
229
|
+
padding: 8px 12px;
|
|
230
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
231
|
+
background: var(--md-sys-color-surface-container-low);
|
|
232
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
line-height: 1.4;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.next-step a {
|
|
238
|
+
display: inline-flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 3px;
|
|
241
|
+
flex: none;
|
|
242
|
+
color: var(--md-sys-color-primary);
|
|
243
|
+
font-weight: 650;
|
|
244
|
+
text-decoration: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.next-step a:hover { text-decoration: underline; }
|
|
248
|
+
.next-step md-icon { --md-icon-size: 16px; }
|
|
249
|
+
ox-assistant-chat[hidden] { display: none; }
|
|
250
|
+
.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); }
|
|
251
|
+
.transition button { display: block; margin-top: 6px; font: inherit; }
|
|
194
252
|
`
|
|
195
253
|
]
|
|
196
254
|
|
|
@@ -211,6 +269,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
211
269
|
@state() private showingHistory = false
|
|
212
270
|
@state() private conversationView = 0
|
|
213
271
|
@state() private asking?: { ask?: string; draft?: string; requestId: string }
|
|
272
|
+
@state() private screenChanged = false
|
|
273
|
+
@state() private navigationNotice = ''
|
|
274
|
+
private openedIdentity = ''
|
|
214
275
|
|
|
215
276
|
private app = new AppController(this)
|
|
216
277
|
/** The kind the person picked; kept while the surface stays the same. */
|
|
@@ -237,11 +298,24 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
237
298
|
|
|
238
299
|
private onStartNewSession = () => void this.newConversation()
|
|
239
300
|
|
|
301
|
+
/** Who reads the open conversation - absent until the list or the create answer says. */
|
|
302
|
+
private get audience(): string | undefined {
|
|
303
|
+
return this.sessions.find(session => session.id === this.sessionId)?.visibility || undefined
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected updated(changed: Map<PropertyKey, unknown>) {
|
|
307
|
+
super.updated?.(changed as any)
|
|
308
|
+
const audience = !this.showingHistory && this.audience === 'open' ? 'open' : null
|
|
309
|
+
if (audience) this.setAttribute('audience', audience)
|
|
310
|
+
else this.removeAttribute('audience')
|
|
311
|
+
}
|
|
312
|
+
|
|
240
313
|
connectedCallback() {
|
|
241
314
|
super.connectedCallback()
|
|
242
315
|
this.addEventListener('assistant-start-new-session', this.onStartNewSession)
|
|
243
316
|
this.unsubscribers.push(
|
|
244
317
|
subscribeAssistantSurface(() => this.follow()),
|
|
318
|
+
store.subscribe(() => { if (this.openedIdentity !== this.identityKey()) this.follow() }),
|
|
245
319
|
subscribeAssistantAsk(() => void this.takeAsk()),
|
|
246
320
|
subscribeSessionActivity(activity => {
|
|
247
321
|
/* A rename or a new conversation elsewhere on this anchor - the tabs read again. */
|
|
@@ -260,24 +334,75 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
260
334
|
super.disconnectedCallback()
|
|
261
335
|
}
|
|
262
336
|
|
|
263
|
-
private scopeKey(surface
|
|
337
|
+
private scopeKey(surface: AssistantSurface | undefined) {
|
|
264
338
|
return JSON.stringify([this.app.contextPath, surface?.key ?? ''])
|
|
265
339
|
}
|
|
266
340
|
|
|
341
|
+
private identityKey() {
|
|
342
|
+
const user = (store.getState() as any).auth?.user
|
|
343
|
+
return JSON.stringify([this.app.contextPath, user?.id || user?.email || ''])
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private get navigationState() {
|
|
347
|
+
return (this.renderRoot.querySelector('ox-assistant-chat') as any)?.navigationState ?? { busy: false, draft: false }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async mayLeaveConversation(): Promise<boolean> {
|
|
351
|
+
if (this.navigationState.busy) {
|
|
352
|
+
notify({ level: 'info', message: t('wait-before-switch', 'Wait for the current response before changing conversations.') })
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
if (!this.navigationState.draft) return true
|
|
356
|
+
const identity = this.identityKey()
|
|
357
|
+
const accepted = await OxPrompt.open({
|
|
358
|
+
type: 'warning', title: t('switch-title', 'Change conversation?'),
|
|
359
|
+
text: t('switch-draft', 'Your unsent text and attachments will not move to the other conversation.'),
|
|
360
|
+
confirmButton: { text: t('switch-confirm', 'Discard draft and switch') },
|
|
361
|
+
cancelButton: { text: t('stay', 'Keep writing') }
|
|
362
|
+
})
|
|
363
|
+
return !!accepted && identity === this.identityKey()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async requestSurface(surface: AssistantSurface | undefined) {
|
|
367
|
+
if (!(await this.mayLeaveConversation())) return
|
|
368
|
+
this.chosenKind = ''
|
|
369
|
+
this.surface = surface
|
|
370
|
+
this.navigationNotice = surface ? t('now-about', 'Conversation subject: {name}', { name: surface.label() }) : ''
|
|
371
|
+
await this.open()
|
|
372
|
+
}
|
|
373
|
+
|
|
267
374
|
/** The screen changed - a different surface or domain is a different conversation. */
|
|
268
375
|
private follow(force = false) {
|
|
269
376
|
const next = currentAssistantSurface()
|
|
270
|
-
|
|
271
|
-
|
|
377
|
+
const transition = surfaceTransition({
|
|
378
|
+
identityChanged: this.openedIdentity !== this.identityKey(),
|
|
379
|
+
sameSubject: this.scopeKey(next) === this.openedFor,
|
|
380
|
+
active: isAnchor(this.anchor) && (!!this.sessionId || this.navigationState.draft || this.navigationState.busy)
|
|
381
|
+
})
|
|
382
|
+
if (!force && transition === 'refresh') {
|
|
383
|
+
this.surface = next
|
|
384
|
+
this.screenChanged = false
|
|
272
385
|
this.requestUpdate()
|
|
273
386
|
return
|
|
274
387
|
}
|
|
388
|
+
if (!force && transition === 'offer') {
|
|
389
|
+
this.screenChanged = true
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if (transition === 'reset') {
|
|
393
|
+
this.asking = undefined
|
|
394
|
+
this.capability = 'unknown'
|
|
395
|
+
if (this.openedIdentity) takeAssistantAsk()
|
|
396
|
+
}
|
|
397
|
+
this.navigationNotice = transition === 'follow' && next ? t('now-about', 'Conversation subject: {name}', { name: next.label() }) : ''
|
|
275
398
|
if (next?.key !== this.surface?.key) this.chosenKind = ''
|
|
276
399
|
this.surface = next
|
|
277
400
|
void this.open()
|
|
278
401
|
}
|
|
279
402
|
|
|
280
403
|
private async open(): Promise<void> {
|
|
404
|
+
this.openedIdentity = this.identityKey()
|
|
405
|
+
this.screenChanged = false
|
|
281
406
|
const ticket = ++this.revision
|
|
282
407
|
this.conversationView++
|
|
283
408
|
this.showingHistory = false
|
|
@@ -285,7 +410,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
285
410
|
this.loading = true
|
|
286
411
|
this.failure = ''
|
|
287
412
|
this.anchor = undefined
|
|
288
|
-
this.openedFor = this.scopeKey()
|
|
413
|
+
this.openedFor = this.scopeKey(this.surface)
|
|
289
414
|
|
|
290
415
|
try {
|
|
291
416
|
if (this.capability !== 'configured') {
|
|
@@ -341,13 +466,15 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
341
466
|
|
|
342
467
|
private async newConversation() {
|
|
343
468
|
if (this.loading || !isAnchor(this.anchor)) return
|
|
469
|
+
if (!(await this.mayLeaveConversation())) return
|
|
344
470
|
this.showingHistory = false
|
|
345
471
|
const revision = this.controller.state.revision
|
|
346
472
|
await this.controller.reset()
|
|
347
473
|
if (revision !== this.controller.state.revision) this.conversationView++
|
|
348
474
|
}
|
|
349
475
|
|
|
350
|
-
private selectSession(id: string, pickedKind?: string) {
|
|
476
|
+
private async selectSession(id: string, pickedKind?: string) {
|
|
477
|
+
if (id !== this.sessionId && !(await this.mayLeaveConversation())) return
|
|
351
478
|
this.showingHistory = false
|
|
352
479
|
const follow = followPickedConversation(pickedKind, this.kind)
|
|
353
480
|
if (follow) {
|
|
@@ -360,9 +487,10 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
360
487
|
if (this.controller.select(id) && previous !== id) this.conversationView++
|
|
361
488
|
}
|
|
362
489
|
|
|
363
|
-
private onModeChange = (event: CustomEvent) => {
|
|
490
|
+
private onModeChange = async (event: CustomEvent) => {
|
|
364
491
|
const kind = event.detail?.kind
|
|
365
492
|
if (!kind || kind === this.kind) return
|
|
493
|
+
if (!(await this.mayLeaveConversation())) return
|
|
366
494
|
this.chosenKind = kind
|
|
367
495
|
void this.open()
|
|
368
496
|
}
|
|
@@ -371,6 +499,11 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
371
499
|
|
|
372
500
|
private async takeAsk() {
|
|
373
501
|
if (this.loading || !this.anchor) return
|
|
502
|
+
if (this.screenChanged || this.navigationState.busy || this.navigationState.draft) {
|
|
503
|
+
const waiting = takeAssistantAsk()
|
|
504
|
+
if (waiting) notify({ level: 'info', message: t('ask-kept-current', 'The current conversation was kept. Switch to the intended subject before asking again.') })
|
|
505
|
+
return
|
|
506
|
+
}
|
|
374
507
|
if (!isAnchor(this.anchor)) {
|
|
375
508
|
/*
|
|
376
509
|
A question is not sent later on its own, once some conversation happens to stand - by then the person is looking
|
|
@@ -442,6 +575,8 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
442
575
|
/* ── proposals ───────────────────────────────────────────────────────────────────── */
|
|
443
576
|
|
|
444
577
|
private proposalContext(sessionId?: string): ProposalContext {
|
|
578
|
+
const identity = this.identityKey()
|
|
579
|
+
const scope = this.openedFor
|
|
445
580
|
return {
|
|
446
581
|
sessionId,
|
|
447
582
|
surface: this.surface,
|
|
@@ -452,7 +587,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
452
587
|
text,
|
|
453
588
|
confirmButton: { text: confirmText ?? t('confirm', 'OK') },
|
|
454
589
|
cancelButton: { text: cancelText ?? t('cancel', 'Cancel') }
|
|
455
|
-
})),
|
|
590
|
+
})) && identity === this.identityKey() && scope === this.scopeKey(currentAssistantSurface()),
|
|
456
591
|
notify: (level, message) => notify({ level, message })
|
|
457
592
|
}
|
|
458
593
|
}
|
|
@@ -476,7 +611,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
476
611
|
/** Every proposal ends in exactly one reported outcome - a kind nobody handles is `unavailable`. */
|
|
477
612
|
private onProposal = async (event: CustomEvent) => {
|
|
478
613
|
const { proposal, sessionId, proposalKey } = event.detail ?? {}
|
|
479
|
-
const outcome =
|
|
614
|
+
const outcome: ProposalOutcome = this.openedFor !== this.scopeKey(currentAssistantSurface()) || this.openedIdentity !== this.identityKey()
|
|
615
|
+
? { verdict: 'unavailable', errorCode: 'conversation-screen-changed', note: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') }
|
|
616
|
+
: await runProposal(proposal, this.proposalContext(sessionId))
|
|
480
617
|
if (outcome.verdict === 'unavailable' && outcome.errorCode === 'unknown-proposal-kind') {
|
|
481
618
|
notify({ level: 'error', message: t('proposal-not-runnable', 'this proposal cannot be run on this screen.') })
|
|
482
619
|
}
|
|
@@ -485,6 +622,33 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
485
622
|
if (outcome.note && sessionId) await this.appendNote(sessionId, outcome.note)
|
|
486
623
|
}
|
|
487
624
|
|
|
625
|
+
/**
|
|
626
|
+
* "Check result" - asks the surface what became of a requested proposal and reports only what it learns.
|
|
627
|
+
* Nothing is run: a lost answer never turns into a second application (ADR-0097 Q-D4 ④).
|
|
628
|
+
*/
|
|
629
|
+
private onProposalCheck = async (event: CustomEvent) => {
|
|
630
|
+
const { proposal, sessionId, proposalKey } = event.detail ?? {}
|
|
631
|
+
const ask = this.surface?.proposalOutcome
|
|
632
|
+
if (!ask) {
|
|
633
|
+
notify({ level: 'info', message: t('proposal-result-uncheckable', 'this screen cannot check that result. open the screen where it was applied.') })
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let outcome: ProposalOutcome | undefined
|
|
638
|
+
try {
|
|
639
|
+
outcome = await ask.call(this.surface, proposalKey, proposal)
|
|
640
|
+
} catch {
|
|
641
|
+
outcome = undefined
|
|
642
|
+
}
|
|
643
|
+
if (!outcome?.verdict) {
|
|
644
|
+
notify({ level: 'info', message: t('proposal-result-unknown', 'the result is still unknown. it was not applied again.') })
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await this.report(sessionId, proposalKey, outcome)
|
|
649
|
+
if (outcome.note && sessionId) await this.appendNote(sessionId, outcome.note)
|
|
650
|
+
}
|
|
651
|
+
|
|
488
652
|
private async appendNote(sessionId: string, note: string) {
|
|
489
653
|
try {
|
|
490
654
|
await client.mutate({
|
|
@@ -503,15 +667,22 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
503
667
|
/* ── board surfaces: the chat's scene edits land on the open board ───────────────── */
|
|
504
668
|
|
|
505
669
|
private onScenePatch = async (event: CustomEvent) => {
|
|
670
|
+
if (!this.mayApplyOnScreen()) return
|
|
506
671
|
const board = this.surface?.board
|
|
507
672
|
if (!board) return
|
|
508
673
|
const result = await board.applyPatch(event.detail)
|
|
509
674
|
if (result?.missed) notify({ level: 'info', message: t('patch-missed', '{missed} of the changes could not be applied.', { missed: result.missed }) })
|
|
510
675
|
}
|
|
511
676
|
|
|
512
|
-
private
|
|
677
|
+
private mayApplyOnScreen() {
|
|
678
|
+
if (this.openedFor === this.scopeKey(currentAssistantSurface()) && this.openedIdentity === this.identityKey()) return true
|
|
679
|
+
notify({ level: 'info', message: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') })
|
|
680
|
+
return false
|
|
681
|
+
}
|
|
513
682
|
|
|
514
|
-
private
|
|
683
|
+
private onBoardRevert = (event: CustomEvent) => { if (this.mayApplyOnScreen()) void this.surface?.board?.revertPatch?.(event.detail?.patchId) }
|
|
684
|
+
|
|
685
|
+
private onSceneAction = (event: CustomEvent) => { if (this.mayApplyOnScreen()) void this.surface?.board?.applyAction?.(event.detail) }
|
|
515
686
|
|
|
516
687
|
/* ── drawing ─────────────────────────────────────────────────────────────────────── */
|
|
517
688
|
|
|
@@ -540,16 +711,16 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
540
711
|
<md-icon>home</md-icon>
|
|
541
712
|
</button>`
|
|
542
713
|
: nothing}
|
|
543
|
-
${ready
|
|
714
|
+
${ready && this.showingHistory
|
|
544
715
|
? html`<button
|
|
545
716
|
class="sbtn"
|
|
546
717
|
title=${this.showingHistory ? t('back-to-conversation', 'back to the conversation') : t('conversations', 'conversations')}
|
|
547
718
|
@click=${() => (this.showingHistory = !this.showingHistory)}
|
|
548
719
|
>
|
|
549
|
-
<md-icon>${
|
|
720
|
+
<md-icon>arrow_back</md-icon>${t('back-to-conversation', 'back to the conversation')}
|
|
550
721
|
</button>`
|
|
551
722
|
: nothing}
|
|
552
|
-
<button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.
|
|
723
|
+
<button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.requestSurface(currentAssistantSurface())}>
|
|
553
724
|
<md-icon>refresh</md-icon>
|
|
554
725
|
</button>
|
|
555
726
|
<button class="sbtn" title=${t('close', 'close')} @click=${() => closeOverlay(this.viewpart)}>
|
|
@@ -561,9 +732,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
561
732
|
|
|
562
733
|
/** The home button: talk about the application instead of this screen, until the screen changes. */
|
|
563
734
|
private useSurface(surface: AssistantSurface) {
|
|
564
|
-
this.surface
|
|
565
|
-
this.chosenKind = ''
|
|
566
|
-
void this.open()
|
|
735
|
+
void this.requestSurface(surface)
|
|
567
736
|
}
|
|
568
737
|
|
|
569
738
|
render(): TemplateResult {
|
|
@@ -578,6 +747,14 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
578
747
|
|
|
579
748
|
return html`
|
|
580
749
|
${this.header()}
|
|
750
|
+
${surface?.nextStep ? html`<div class="next-step" role="status">
|
|
751
|
+
<span>${surface.nextStep.description?.() || ''}</span>
|
|
752
|
+
<a href=${surface.nextStep.href}>${surface.nextStep.label()}<md-icon>arrow_forward</md-icon></a>
|
|
753
|
+
</div>` : nothing}
|
|
754
|
+
${this.screenChanged ? html`<div class="transition" role="status">
|
|
755
|
+
${t('screen-and-conversation', 'Screen: {screen}. Conversation: {conversation}. Your conversation has been kept.', { screen: currentAssistantSurface()?.label() || t('no-subject', 'No subject'), conversation: surface?.label() || '' })}
|
|
756
|
+
<button class="sbtn" @click=${() => this.requestSurface(currentAssistantSurface())}>${t('switch-to-screen', 'Talk about this screen')}</button>
|
|
757
|
+
</div>` : this.navigationNotice ? html`<div class="transition" role="status">${this.navigationNotice}</div>` : nothing}
|
|
581
758
|
${ready && this.showingHistory
|
|
582
759
|
? html`<ox-assistant-session-manager
|
|
583
760
|
.anchorType=${(anchor as any).anchorType}
|
|
@@ -585,6 +762,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
585
762
|
.sessionId=${this.sessionId}
|
|
586
763
|
.kind=${this.kind}
|
|
587
764
|
.kindLabels=${this.kindLabels}
|
|
765
|
+
.scopeLabel=${t('conversation-scope', 'Conversations · {name}', { name: surface?.label() || '' })}
|
|
588
766
|
@assistant-session-select=${(event: CustomEvent) => this.selectSession(event.detail.sessionId, event.detail.kind)}
|
|
589
767
|
@assistant-session-reset=${() => this.newConversation()}
|
|
590
768
|
@assistant-session-changed=${() => this.reloadSessions()}
|
|
@@ -597,21 +775,30 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
597
775
|
.loading=${this.loading}
|
|
598
776
|
.error=${this.failure}
|
|
599
777
|
historyEnabled
|
|
778
|
+
singleConversation
|
|
600
779
|
@assistant-session-select=${(event: CustomEvent) => this.selectSession(event.detail.sessionId)}
|
|
601
780
|
@assistant-session-reset=${() => this.newConversation()}
|
|
602
781
|
@assistant-session-history=${() => (this.showingHistory = true)}
|
|
603
782
|
@assistant-session-retry=${() => this.open()}
|
|
604
783
|
></ox-assistant-session-toolbar>`
|
|
605
784
|
: nothing}
|
|
606
|
-
${ready && !this.showingHistory &&
|
|
785
|
+
${ready && !this.showingHistory && this.audience === 'open'
|
|
786
|
+
? html`<div class="audience" role="status">
|
|
787
|
+
<md-icon>public</md-icon>
|
|
788
|
+
<span>${t('audience-open-band', 'Public conversation - everyone in this domain can read and write here.')}</span>
|
|
789
|
+
</div>`
|
|
790
|
+
: nothing}
|
|
791
|
+
${ready && !this.loading && !this.failure
|
|
607
792
|
? keyed(
|
|
608
793
|
this.conversationView,
|
|
609
794
|
html`<ox-assistant-chat
|
|
795
|
+
?hidden=${this.showingHistory}
|
|
796
|
+
.sendBlocked=${this.screenChanged ? t('return-to-subject', 'Return to the conversation subject before sending or applying changes.') : ''}
|
|
610
797
|
.sessionId=${this.sessionId}
|
|
611
798
|
.prepareSession=${(adopt: (id: string) => void) => this.controller.prepareSession(adopt)}
|
|
612
799
|
.contextProvider=${() => {
|
|
613
800
|
/* The screen moved on between writing and sending - do not answer about the old one. */
|
|
614
|
-
if (openedFor !== this.scopeKey(currentAssistantSurface()
|
|
801
|
+
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
802
|
return { hostContext: surface?.hostContext?.() ?? {} }
|
|
616
803
|
}}
|
|
617
804
|
.mode=${mode}
|
|
@@ -631,6 +818,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
631
818
|
.draftRequestId=${this.asking?.requestId}
|
|
632
819
|
@assistant-mode-change=${this.onModeChange}
|
|
633
820
|
@assistant-proposal-execute=${this.onProposal}
|
|
821
|
+
@assistant-proposal-check=${this.onProposalCheck}
|
|
634
822
|
@scene-edit-patch=${this.onScenePatch}
|
|
635
823
|
@board-edit-revert=${this.onBoardRevert}
|
|
636
824
|
@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
|
+
})
|