@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.
Files changed (89) hide show
  1. package/client/components/assistant-chat.ts +102 -13
  2. package/client/components/assistant-dock.ts +206 -18
  3. package/client/components/assistant-mode.test.ts +16 -2
  4. package/client/components/assistant-session-manager.ts +58 -26
  5. package/client/components/assistant-session-toolbar.ts +4 -2
  6. package/client/components/chat-echo-dedup.ts +4 -0
  7. package/client/components/proposal-outcome-line.ts +36 -0
  8. package/client/components/proposal-request.test.ts +52 -0
  9. package/client/utils/assistant-navigation.test.ts +38 -0
  10. package/client/utils/assistant-navigation.ts +6 -0
  11. package/client/utils/assistant-session-controller.ts +2 -0
  12. package/client/utils/assistant-session-transport.ts +2 -2
  13. package/client/utils/assistant-surface.ts +21 -0
  14. package/dist-client/components/assistant-chat.d.ts +15 -0
  15. package/dist-client/components/assistant-chat.js +106 -13
  16. package/dist-client/components/assistant-chat.js.map +1 -1
  17. package/dist-client/components/assistant-dock.d.ts +16 -0
  18. package/dist-client/components/assistant-dock.js +221 -20
  19. package/dist-client/components/assistant-dock.js.map +1 -1
  20. package/dist-client/components/assistant-mode.test.js +14 -2
  21. package/dist-client/components/assistant-mode.test.js.map +1 -1
  22. package/dist-client/components/assistant-session-manager.d.ts +4 -0
  23. package/dist-client/components/assistant-session-manager.js +74 -27
  24. package/dist-client/components/assistant-session-manager.js.map +1 -1
  25. package/dist-client/components/assistant-session-toolbar.d.ts +1 -0
  26. package/dist-client/components/assistant-session-toolbar.js +8 -2
  27. package/dist-client/components/assistant-session-toolbar.js.map +1 -1
  28. package/dist-client/components/chat-echo-dedup.d.ts +2 -0
  29. package/dist-client/components/chat-echo-dedup.js +2 -0
  30. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  31. package/dist-client/components/proposal-outcome-line.d.ts +29 -0
  32. package/dist-client/components/proposal-outcome-line.js +15 -0
  33. package/dist-client/components/proposal-outcome-line.js.map +1 -1
  34. package/dist-client/components/proposal-request.test.d.ts +1 -0
  35. package/dist-client/components/proposal-request.test.js +45 -0
  36. package/dist-client/components/proposal-request.test.js.map +1 -0
  37. package/dist-client/tsconfig.tsbuildinfo +1 -1
  38. package/dist-client/utils/assistant-navigation.d.ts +6 -0
  39. package/dist-client/utils/assistant-navigation.js +9 -0
  40. package/dist-client/utils/assistant-navigation.js.map +1 -0
  41. package/dist-client/utils/assistant-navigation.test.d.ts +1 -0
  42. package/dist-client/utils/assistant-navigation.test.js +37 -0
  43. package/dist-client/utils/assistant-navigation.test.js.map +1 -0
  44. package/dist-client/utils/assistant-session-controller.d.ts +2 -0
  45. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  46. package/dist-client/utils/assistant-session-transport.js +2 -2
  47. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  48. package/dist-client/utils/assistant-surface.d.ts +19 -0
  49. package/dist-client/utils/assistant-surface.js.map +1 -1
  50. package/dist-server/service/assistant-chat-resolver.js +1 -0
  51. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  52. package/dist-server/service/chat-message/chat-message.d.ts +10 -0
  53. package/dist-server/service/chat-message/chat-message.js +29 -0
  54. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  55. package/dist-server/service/chat-message/proposal-outcome.d.ts +8 -0
  56. package/dist-server/service/chat-message/proposal-outcome.js +35 -1
  57. package/dist-server/service/chat-message/proposal-outcome.js.map +1 -1
  58. package/dist-server/service/chat-session/chat-session.d.ts +5 -5
  59. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  60. package/dist-server/service/chat-session/session-inbox.d.ts +7 -0
  61. package/dist-server/service/chat-session/session-inbox.js +7 -0
  62. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  63. package/dist-server/service/chat-session/session-visibility.d.ts +8 -5
  64. package/dist-server/service/chat-session/session-visibility.js +9 -15
  65. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  66. package/dist-server/service/chat-session-resolver.d.ts +1 -0
  67. package/dist-server/service/chat-session-resolver.js +64 -10
  68. package/dist-server/service/chat-session-resolver.js.map +1 -1
  69. package/dist-server/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +3 -3
  71. package/server/service/assistant-chat-resolver.ts +1 -0
  72. package/server/service/chat-message/chat-message.ts +32 -0
  73. package/server/service/chat-message/proposal-outcome.test.ts +53 -2
  74. package/server/service/chat-message/proposal-outcome.ts +33 -0
  75. package/server/service/chat-session/chat-session.ts +5 -5
  76. package/server/service/chat-session/session-inbox.ts +7 -0
  77. package/server/service/chat-session/session-visibility.test.ts +6 -6
  78. package/server/service/chat-session/session-visibility.ts +9 -15
  79. package/server/service/chat-session-resolver.ts +72 -13
  80. package/server/service/session-anchor.test.ts +2 -2
  81. package/test/assistant-dock.test.ts +8 -3
  82. package/test/assistant-session-toolbar.test.ts +7 -1
  83. package/test/open-doors.test.ts +2 -1
  84. package/test/session-discovery.test.ts +42 -0
  85. package/translations/en.json +28 -2
  86. package/translations/ja.json +28 -2
  87. package/translations/ko.json +28 -2
  88. package/translations/ms.json +28 -2
  89. package/translations/zh.json +28 -2
@@ -35,6 +35,9 @@ export declare class AssistantDock extends AssistantDock_base {
35
35
  private showingHistory;
36
36
  private conversationView;
37
37
  private asking?;
38
+ private screenChanged;
39
+ private navigationNotice;
40
+ private openedIdentity;
38
41
  private app;
39
42
  /** The kind the person picked; kept while the surface stays the same. */
40
43
  private chosenKind;
@@ -44,9 +47,16 @@ export declare class AssistantDock extends AssistantDock_base {
44
47
  private unsubscribers;
45
48
  private controller;
46
49
  private onStartNewSession;
50
+ /** Who reads the open conversation - absent until the list or the create answer says. */
51
+ private get audience();
52
+ protected updated(changed: Map<PropertyKey, unknown>): void;
47
53
  connectedCallback(): void;
48
54
  disconnectedCallback(): void;
49
55
  private scopeKey;
56
+ private identityKey;
57
+ private get navigationState();
58
+ private mayLeaveConversation;
59
+ private requestSurface;
50
60
  /** The screen changed - a different surface or domain is a different conversation. */
51
61
  private follow;
52
62
  private open;
@@ -62,8 +72,14 @@ export declare class AssistantDock extends AssistantDock_base {
62
72
  private report;
63
73
  /** Every proposal ends in exactly one reported outcome - a kind nobody handles is `unavailable`. */
64
74
  private onProposal;
75
+ /**
76
+ * "Check result" - asks the surface what became of a requested proposal and reports only what it learns.
77
+ * Nothing is run: a lost answer never turns into a second application (ADR-0097 Q-D4 ④).
78
+ */
79
+ private onProposalCheck;
65
80
  private appendNote;
66
81
  private onScenePatch;
82
+ private mayApplyOnScreen;
67
83
  private onBoardRevert;
68
84
  private onSceneAction;
69
85
  private stuckLine;
@@ -21,6 +21,7 @@ import { setOpenSession, subscribeSessionActivity } from '../utils/assistant-act
21
21
  import { assistantHomeSurface, currentAssistantSurface, isAnchor, subscribeAssistantSurface } from '../utils/assistant-surface.js';
22
22
  import { runProposal } from '../utils/assistant-proposals.js';
23
23
  import { holdAssistantAsk, subscribeAssistantAsk, takeAssistantAsk } from '../utils/assistant-ask.js';
24
+ import { surfaceTransition } from '../utils/assistant-navigation.js';
24
25
  export const ASSISTANT_DOCK_VIEWPART = 'assistant-dock';
25
26
  const t = (key, defaultValue, options = {}) => i18next.t(`ai-assistant.dock.${key}`, { defaultValue, ...options });
26
27
  const MODES_QUERY = gql `
@@ -66,6 +67,9 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
66
67
  this.failure = '';
67
68
  this.showingHistory = false;
68
69
  this.conversationView = 0;
70
+ this.screenChanged = false;
71
+ this.navigationNotice = '';
72
+ this.openedIdentity = '';
69
73
  this.app = new AppController(this);
70
74
  /** The kind the person picked; kept while the surface stays the same. */
71
75
  this.chosenKind = '';
@@ -84,17 +88,21 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
84
88
  this.failure = next.error;
85
89
  }, { getItem: key => sessionStorage.getItem(key), setItem: (key, value) => sessionStorage.setItem(key, value) }, { lazy: true });
86
90
  this.onStartNewSession = () => void this.newConversation();
87
- this.onModeChange = (event) => {
91
+ this.onModeChange = async (event) => {
88
92
  const kind = event.detail?.kind;
89
93
  if (!kind || kind === this.kind)
90
94
  return;
95
+ if (!(await this.mayLeaveConversation()))
96
+ return;
91
97
  this.chosenKind = kind;
92
98
  void this.open();
93
99
  };
94
100
  /** Every proposal ends in exactly one reported outcome - a kind nobody handles is `unavailable`. */
95
101
  this.onProposal = async (event) => {
96
102
  const { proposal, sessionId, proposalKey } = event.detail ?? {};
97
- const outcome = await runProposal(proposal, this.proposalContext(sessionId));
103
+ const outcome = this.openedFor !== this.scopeKey(currentAssistantSurface()) || this.openedIdentity !== this.identityKey()
104
+ ? { verdict: 'unavailable', errorCode: 'conversation-screen-changed', note: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') }
105
+ : await runProposal(proposal, this.proposalContext(sessionId));
98
106
  if (outcome.verdict === 'unavailable' && outcome.errorCode === 'unknown-proposal-kind') {
99
107
  notify({ level: 'error', message: t('proposal-not-runnable', 'this proposal cannot be run on this screen.') });
100
108
  }
@@ -103,8 +111,36 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
103
111
  if (outcome.note && sessionId)
104
112
  await this.appendNote(sessionId, outcome.note);
105
113
  };
114
+ /**
115
+ * "Check result" - asks the surface what became of a requested proposal and reports only what it learns.
116
+ * Nothing is run: a lost answer never turns into a second application (ADR-0097 Q-D4 ④).
117
+ */
118
+ this.onProposalCheck = async (event) => {
119
+ const { proposal, sessionId, proposalKey } = event.detail ?? {};
120
+ const ask = this.surface?.proposalOutcome;
121
+ if (!ask) {
122
+ notify({ level: 'info', message: t('proposal-result-uncheckable', 'this screen cannot check that result. open the screen where it was applied.') });
123
+ return;
124
+ }
125
+ let outcome;
126
+ try {
127
+ outcome = await ask.call(this.surface, proposalKey, proposal);
128
+ }
129
+ catch {
130
+ outcome = undefined;
131
+ }
132
+ if (!outcome?.verdict) {
133
+ notify({ level: 'info', message: t('proposal-result-unknown', 'the result is still unknown. it was not applied again.') });
134
+ return;
135
+ }
136
+ await this.report(sessionId, proposalKey, outcome);
137
+ if (outcome.note && sessionId)
138
+ await this.appendNote(sessionId, outcome.note);
139
+ };
106
140
  /* ── board surfaces: the chat's scene edits land on the open board ───────────────── */
107
141
  this.onScenePatch = async (event) => {
142
+ if (!this.mayApplyOnScreen())
143
+ return;
108
144
  const board = this.surface?.board;
109
145
  if (!board)
110
146
  return;
@@ -112,8 +148,10 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
112
148
  if (result?.missed)
113
149
  notify({ level: 'info', message: t('patch-missed', '{missed} of the changes could not be applied.', { missed: result.missed }) });
114
150
  };
115
- this.onBoardRevert = (event) => void this.surface?.board?.revertPatch?.(event.detail?.patchId);
116
- this.onSceneAction = (event) => void this.surface?.board?.applyAction?.(event.detail);
151
+ this.onBoardRevert = (event) => { if (this.mayApplyOnScreen())
152
+ void this.surface?.board?.revertPatch?.(event.detail?.patchId); };
153
+ this.onSceneAction = (event) => { if (this.mayApplyOnScreen())
154
+ void this.surface?.board?.applyAction?.(event.detail); };
117
155
  }
118
156
  static { this.styles = [
119
157
  ScrollbarStyles,
@@ -207,6 +245,34 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
207
245
  --md-icon-size: 18px;
208
246
  }
209
247
 
248
+ /*
249
+ * An open conversation is read by everyone in the domain - the dock says so on every screen of it, not
250
+ * only in the list (2026-09-30: open and private were one grey word apart, and an owner's question about
251
+ * the books sat in every employee's list). A private one draws nothing extra: it is the default.
252
+ */
253
+ :host([audience='open']) {
254
+ border-left: 3px solid var(--md-sys-color-tertiary);
255
+ }
256
+
257
+ .audience {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 6px;
261
+ flex: none;
262
+ padding: 6px 10px;
263
+ background: var(--md-sys-color-tertiary-container);
264
+ color: var(--md-sys-color-on-tertiary-container);
265
+ border-bottom: 1px solid var(--md-sys-color-tertiary);
266
+ font-size: 12px;
267
+ font-weight: 600;
268
+ line-height: 1.35;
269
+ }
270
+
271
+ .audience md-icon {
272
+ --md-icon-size: 16px;
273
+ flex: none;
274
+ }
275
+
210
276
  ox-assistant-session-toolbar {
211
277
  flex: none;
212
278
  min-width: 0;
@@ -239,12 +305,54 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
239
305
  gap: 4px;
240
306
  font: inherit;
241
307
  }
308
+
309
+ .next-step {
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ gap: 10px;
314
+ padding: 8px 12px;
315
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
316
+ background: var(--md-sys-color-surface-container-low);
317
+ color: var(--md-sys-color-on-surface-variant);
318
+ font-size: 12px;
319
+ line-height: 1.4;
320
+ }
321
+
322
+ .next-step a {
323
+ display: inline-flex;
324
+ align-items: center;
325
+ gap: 3px;
326
+ flex: none;
327
+ color: var(--md-sys-color-primary);
328
+ font-weight: 650;
329
+ text-decoration: none;
330
+ }
331
+
332
+ .next-step a:hover { text-decoration: underline; }
333
+ .next-step md-icon { --md-icon-size: 16px; }
334
+ ox-assistant-chat[hidden] { display: none; }
335
+ .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); }
336
+ .transition button { display: block; margin-top: 6px; font: inherit; }
242
337
  `
243
338
  ]; }
339
+ /** Who reads the open conversation - absent until the list or the create answer says. */
340
+ get audience() {
341
+ return this.sessions.find(session => session.id === this.sessionId)?.visibility || undefined;
342
+ }
343
+ updated(changed) {
344
+ super.updated?.(changed);
345
+ const audience = !this.showingHistory && this.audience === 'open' ? 'open' : null;
346
+ if (audience)
347
+ this.setAttribute('audience', audience);
348
+ else
349
+ this.removeAttribute('audience');
350
+ }
244
351
  connectedCallback() {
245
352
  super.connectedCallback();
246
353
  this.addEventListener('assistant-start-new-session', this.onStartNewSession);
247
- this.unsubscribers.push(subscribeAssistantSurface(() => this.follow()), subscribeAssistantAsk(() => void this.takeAsk()), subscribeSessionActivity(activity => {
354
+ this.unsubscribers.push(subscribeAssistantSurface(() => this.follow()), store.subscribe(() => { if (this.openedIdentity !== this.identityKey())
355
+ this.follow(); }), subscribeAssistantAsk(() => void this.takeAsk()), subscribeSessionActivity(activity => {
248
356
  /* A rename or a new conversation elsewhere on this anchor - the tabs read again. */
249
357
  if (activity.kind === 'meta' && isAnchor(this.anchor) && activity.anchorId === this.anchor.anchorId)
250
358
  void this.reloadSessions();
@@ -259,23 +367,73 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
259
367
  this.controller.clear();
260
368
  super.disconnectedCallback();
261
369
  }
262
- scopeKey(surface = this.surface) {
370
+ scopeKey(surface) {
263
371
  return JSON.stringify([this.app.contextPath, surface?.key ?? '']);
264
372
  }
373
+ identityKey() {
374
+ const user = store.getState().auth?.user;
375
+ return JSON.stringify([this.app.contextPath, user?.id || user?.email || '']);
376
+ }
377
+ get navigationState() {
378
+ return this.renderRoot.querySelector('ox-assistant-chat')?.navigationState ?? { busy: false, draft: false };
379
+ }
380
+ async mayLeaveConversation() {
381
+ if (this.navigationState.busy) {
382
+ notify({ level: 'info', message: t('wait-before-switch', 'Wait for the current response before changing conversations.') });
383
+ return false;
384
+ }
385
+ if (!this.navigationState.draft)
386
+ return true;
387
+ const identity = this.identityKey();
388
+ const accepted = await OxPrompt.open({
389
+ type: 'warning', title: t('switch-title', 'Change conversation?'),
390
+ text: t('switch-draft', 'Your unsent text and attachments will not move to the other conversation.'),
391
+ confirmButton: { text: t('switch-confirm', 'Discard draft and switch') },
392
+ cancelButton: { text: t('stay', 'Keep writing') }
393
+ });
394
+ return !!accepted && identity === this.identityKey();
395
+ }
396
+ async requestSurface(surface) {
397
+ if (!(await this.mayLeaveConversation()))
398
+ return;
399
+ this.chosenKind = '';
400
+ this.surface = surface;
401
+ this.navigationNotice = surface ? t('now-about', 'Conversation subject: {name}', { name: surface.label() }) : '';
402
+ await this.open();
403
+ }
265
404
  /** The screen changed - a different surface or domain is a different conversation. */
266
405
  follow(force = false) {
267
406
  const next = currentAssistantSurface();
268
- if (!force && next === this.surface && this.scopeKey(next) === this.openedFor) {
269
- /* The same conversation, told that its words changed - a name loaded, a board renamed on save. */
407
+ const transition = surfaceTransition({
408
+ identityChanged: this.openedIdentity !== this.identityKey(),
409
+ sameSubject: this.scopeKey(next) === this.openedFor,
410
+ active: isAnchor(this.anchor) && (!!this.sessionId || this.navigationState.draft || this.navigationState.busy)
411
+ });
412
+ if (!force && transition === 'refresh') {
413
+ this.surface = next;
414
+ this.screenChanged = false;
270
415
  this.requestUpdate();
271
416
  return;
272
417
  }
418
+ if (!force && transition === 'offer') {
419
+ this.screenChanged = true;
420
+ return;
421
+ }
422
+ if (transition === 'reset') {
423
+ this.asking = undefined;
424
+ this.capability = 'unknown';
425
+ if (this.openedIdentity)
426
+ takeAssistantAsk();
427
+ }
428
+ this.navigationNotice = transition === 'follow' && next ? t('now-about', 'Conversation subject: {name}', { name: next.label() }) : '';
273
429
  if (next?.key !== this.surface?.key)
274
430
  this.chosenKind = '';
275
431
  this.surface = next;
276
432
  void this.open();
277
433
  }
278
434
  async open() {
435
+ this.openedIdentity = this.identityKey();
436
+ this.screenChanged = false;
279
437
  const ticket = ++this.revision;
280
438
  this.conversationView++;
281
439
  this.showingHistory = false;
@@ -283,7 +441,7 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
283
441
  this.loading = true;
284
442
  this.failure = '';
285
443
  this.anchor = undefined;
286
- this.openedFor = this.scopeKey();
444
+ this.openedFor = this.scopeKey(this.surface);
287
445
  try {
288
446
  if (this.capability !== 'configured') {
289
447
  this.capability = await readAiCapability();
@@ -342,13 +500,17 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
342
500
  async newConversation() {
343
501
  if (this.loading || !isAnchor(this.anchor))
344
502
  return;
503
+ if (!(await this.mayLeaveConversation()))
504
+ return;
345
505
  this.showingHistory = false;
346
506
  const revision = this.controller.state.revision;
347
507
  await this.controller.reset();
348
508
  if (revision !== this.controller.state.revision)
349
509
  this.conversationView++;
350
510
  }
351
- selectSession(id, pickedKind) {
511
+ async selectSession(id, pickedKind) {
512
+ if (id !== this.sessionId && !(await this.mayLeaveConversation()))
513
+ return;
352
514
  this.showingHistory = false;
353
515
  const follow = followPickedConversation(pickedKind, this.kind);
354
516
  if (follow) {
@@ -365,6 +527,12 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
365
527
  async takeAsk() {
366
528
  if (this.loading || !this.anchor)
367
529
  return;
530
+ if (this.screenChanged || this.navigationState.busy || this.navigationState.draft) {
531
+ const waiting = takeAssistantAsk();
532
+ if (waiting)
533
+ notify({ level: 'info', message: t('ask-kept-current', 'The current conversation was kept. Switch to the intended subject before asking again.') });
534
+ return;
535
+ }
368
536
  if (!isAnchor(this.anchor)) {
369
537
  /*
370
538
  A question is not sent later on its own, once some conversation happens to stand - by then the person is looking
@@ -438,6 +606,8 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
438
606
  }
439
607
  /* ── proposals ───────────────────────────────────────────────────────────────────── */
440
608
  proposalContext(sessionId) {
609
+ const identity = this.identityKey();
610
+ const scope = this.openedFor;
441
611
  return {
442
612
  sessionId,
443
613
  surface: this.surface,
@@ -447,7 +617,7 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
447
617
  text,
448
618
  confirmButton: { text: confirmText ?? t('confirm', 'OK') },
449
619
  cancelButton: { text: cancelText ?? t('cancel', 'Cancel') }
450
- })),
620
+ })) && identity === this.identityKey() && scope === this.scopeKey(currentAssistantSurface()),
451
621
  notify: (level, message) => notify({ level, message })
452
622
  };
453
623
  }
@@ -483,6 +653,12 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
483
653
  /* the outcome is already reported; a lost note is not a lost outcome */
484
654
  }
485
655
  }
656
+ mayApplyOnScreen() {
657
+ if (this.openedFor === this.scopeKey(currentAssistantSurface()) && this.openedIdentity === this.identityKey())
658
+ return true;
659
+ notify({ level: 'info', message: t('return-to-subject', 'Return to the conversation subject before applying this proposal.') });
660
+ return false;
661
+ }
486
662
  /* ── drawing ─────────────────────────────────────────────────────────────────────── */
487
663
  stuckLine() {
488
664
  if (this.capability === 'not-configured')
@@ -512,16 +688,16 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
512
688
  <md-icon>home</md-icon>
513
689
  </button>`
514
690
  : nothing}
515
- ${ready
691
+ ${ready && this.showingHistory
516
692
  ? html `<button
517
693
  class="sbtn"
518
694
  title=${this.showingHistory ? t('back-to-conversation', 'back to the conversation') : t('conversations', 'conversations')}
519
695
  @click=${() => (this.showingHistory = !this.showingHistory)}
520
696
  >
521
- <md-icon>${this.showingHistory ? 'forum' : 'list'}</md-icon>
697
+ <md-icon>arrow_back</md-icon>${t('back-to-conversation', 'back to the conversation')}
522
698
  </button>`
523
699
  : nothing}
524
- <button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.follow(true)}>
700
+ <button class="sbtn" title=${t('rebind', 'follow this screen again')} @click=${() => this.requestSurface(currentAssistantSurface())}>
525
701
  <md-icon>refresh</md-icon>
526
702
  </button>
527
703
  <button class="sbtn" title=${t('close', 'close')} @click=${() => closeOverlay(this.viewpart)}>
@@ -532,9 +708,7 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
532
708
  }
533
709
  /** The home button: talk about the application instead of this screen, until the screen changes. */
534
710
  useSurface(surface) {
535
- this.surface = surface;
536
- this.chosenKind = '';
537
- void this.open();
711
+ void this.requestSurface(surface);
538
712
  }
539
713
  render() {
540
714
  const anchor = this.anchor;
@@ -546,6 +720,14 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
546
720
  const mode = ready ? { kind: this.kind, target: anchor.anchorId, hostContext: {} } : undefined;
547
721
  return html `
548
722
  ${this.header()}
723
+ ${surface?.nextStep ? html `<div class="next-step" role="status">
724
+ <span>${surface.nextStep.description?.() || ''}</span>
725
+ <a href=${surface.nextStep.href}>${surface.nextStep.label()}<md-icon>arrow_forward</md-icon></a>
726
+ </div>` : nothing}
727
+ ${this.screenChanged ? html `<div class="transition" role="status">
728
+ ${t('screen-and-conversation', 'Screen: {screen}. Conversation: {conversation}. Your conversation has been kept.', { screen: currentAssistantSurface()?.label() || t('no-subject', 'No subject'), conversation: surface?.label() || '' })}
729
+ <button class="sbtn" @click=${() => this.requestSurface(currentAssistantSurface())}>${t('switch-to-screen', 'Talk about this screen')}</button>
730
+ </div>` : this.navigationNotice ? html `<div class="transition" role="status">${this.navigationNotice}</div>` : nothing}
549
731
  ${ready && this.showingHistory
550
732
  ? html `<ox-assistant-session-manager
551
733
  .anchorType=${anchor.anchorType}
@@ -553,6 +735,7 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
553
735
  .sessionId=${this.sessionId}
554
736
  .kind=${this.kind}
555
737
  .kindLabels=${this.kindLabels}
738
+ .scopeLabel=${t('conversation-scope', 'Conversations · {name}', { name: surface?.label() || '' })}
556
739
  @assistant-session-select=${(event) => this.selectSession(event.detail.sessionId, event.detail.kind)}
557
740
  @assistant-session-reset=${() => this.newConversation()}
558
741
  @assistant-session-changed=${() => this.reloadSessions()}
@@ -565,20 +748,29 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
565
748
  .loading=${this.loading}
566
749
  .error=${this.failure}
567
750
  historyEnabled
751
+ singleConversation
568
752
  @assistant-session-select=${(event) => this.selectSession(event.detail.sessionId)}
569
753
  @assistant-session-reset=${() => this.newConversation()}
570
754
  @assistant-session-history=${() => (this.showingHistory = true)}
571
755
  @assistant-session-retry=${() => this.open()}
572
756
  ></ox-assistant-session-toolbar>`
573
757
  : nothing}
574
- ${ready && !this.showingHistory && !this.loading && !this.failure
758
+ ${ready && !this.showingHistory && this.audience === 'open'
759
+ ? html `<div class="audience" role="status">
760
+ <md-icon>public</md-icon>
761
+ <span>${t('audience-open-band', 'Public conversation - everyone in this domain can read and write here.')}</span>
762
+ </div>`
763
+ : nothing}
764
+ ${ready && !this.loading && !this.failure
575
765
  ? keyed(this.conversationView, html `<ox-assistant-chat
766
+ ?hidden=${this.showingHistory}
767
+ .sendBlocked=${this.screenChanged ? t('return-to-subject', 'Return to the conversation subject before sending or applying changes.') : ''}
576
768
  .sessionId=${this.sessionId}
577
769
  .prepareSession=${(adopt) => this.controller.prepareSession(adopt)}
578
770
  .contextProvider=${() => {
579
771
  /* The screen moved on between writing and sending - do not answer about the old one. */
580
- if (openedFor !== this.scopeKey(currentAssistantSurface() ?? surface))
581
- throw new Error(t('screen-changed', 'the screen changed while you were writing. send it again.'));
772
+ if (openedFor !== this.scopeKey(currentAssistantSurface()) || this.openedIdentity !== this.identityKey())
773
+ throw new Error(t('return-to-subject', 'Return to the conversation subject before applying this proposal.'));
582
774
  return { hostContext: surface?.hostContext?.() ?? {} };
583
775
  }}
584
776
  .mode=${mode}
@@ -598,6 +790,7 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
598
790
  .draftRequestId=${this.asking?.requestId}
599
791
  @assistant-mode-change=${this.onModeChange}
600
792
  @assistant-proposal-execute=${this.onProposal}
793
+ @assistant-proposal-check=${this.onProposalCheck}
601
794
  @scene-edit-patch=${this.onScenePatch}
602
795
  @board-edit-revert=${this.onBoardRevert}
603
796
  @scene-action-execute=${this.onSceneAction}
@@ -676,6 +869,14 @@ __decorate([
676
869
  state(),
677
870
  __metadata("design:type", Object)
678
871
  ], AssistantDock.prototype, "asking", void 0);
872
+ __decorate([
873
+ state(),
874
+ __metadata("design:type", Object)
875
+ ], AssistantDock.prototype, "screenChanged", void 0);
876
+ __decorate([
877
+ state(),
878
+ __metadata("design:type", Object)
879
+ ], AssistantDock.prototype, "navigationNotice", void 0);
679
880
  AssistantDock = __decorate([
680
881
  customElement('ox-assistant-dock')
681
882
  ], AssistantDock);