@things-factory/ai-assistant 10.1.208 → 10.1.210

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 (167) hide show
  1. package/client/components/assistant-chat.ts +238 -9
  2. package/client/components/assistant-dock.ts +9 -4
  3. package/client/components/assistant-invite.ts +138 -0
  4. package/client/components/assistant-session-manager.ts +53 -12
  5. package/client/components/proposal-request.test.ts +10 -0
  6. package/client/components/roster-identity.test.ts +2 -12
  7. package/client/components/roster-identity.ts +0 -6
  8. package/client/utils/assistant-session-actions.ts +3 -1
  9. package/client/utils/assistant-session-admin.ts +9 -2
  10. package/client/utils/assistant-session-controller.ts +1 -1
  11. package/client/utils/assistant-typing.test.ts +33 -0
  12. package/client/utils/assistant-typing.ts +53 -0
  13. package/dist-client/components/assistant-chat.d.ts +28 -0
  14. package/dist-client/components/assistant-chat.js +259 -8
  15. package/dist-client/components/assistant-chat.js.map +1 -1
  16. package/dist-client/components/assistant-dock.js +12 -4
  17. package/dist-client/components/assistant-dock.js.map +1 -1
  18. package/dist-client/components/assistant-invite.d.ts +39 -0
  19. package/dist-client/components/assistant-invite.js +150 -0
  20. package/dist-client/components/assistant-invite.js.map +1 -0
  21. package/dist-client/components/assistant-session-manager.d.ts +5 -1
  22. package/dist-client/components/assistant-session-manager.js +40 -10
  23. package/dist-client/components/assistant-session-manager.js.map +1 -1
  24. package/dist-client/components/proposal-request.test.js +8 -0
  25. package/dist-client/components/proposal-request.test.js.map +1 -1
  26. package/dist-client/components/roster-identity.d.ts +0 -2
  27. package/dist-client/components/roster-identity.js +0 -7
  28. package/dist-client/components/roster-identity.js.map +1 -1
  29. package/dist-client/components/roster-identity.test.js +2 -13
  30. package/dist-client/components/roster-identity.test.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-client/utils/assistant-session-actions.d.ts +1 -0
  33. package/dist-client/utils/assistant-session-actions.js +3 -1
  34. package/dist-client/utils/assistant-session-actions.js.map +1 -1
  35. package/dist-client/utils/assistant-session-admin.d.ts +5 -2
  36. package/dist-client/utils/assistant-session-admin.js +3 -0
  37. package/dist-client/utils/assistant-session-admin.js.map +1 -1
  38. package/dist-client/utils/assistant-session-controller.d.ts +1 -1
  39. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  40. package/dist-client/utils/assistant-typing.d.ts +20 -0
  41. package/dist-client/utils/assistant-typing.js +48 -0
  42. package/dist-client/utils/assistant-typing.js.map +1 -0
  43. package/dist-client/utils/assistant-typing.test.d.ts +1 -0
  44. package/dist-client/utils/assistant-typing.test.js +30 -0
  45. package/dist-client/utils/assistant-typing.test.js.map +1 -0
  46. package/dist-server/service/assistant-chat-resolver.js +5 -12
  47. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  48. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  49. package/dist-server/service/assistant-mode/index.js +2 -1
  50. package/dist-server/service/assistant-mode/index.js.map +1 -1
  51. package/dist-server/service/assistant-mode/mode-categories.js +9 -2
  52. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  53. package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
  54. package/dist-server/service/assistant-mode/screen-rules.js +10 -3
  55. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
  56. package/dist-server/service/chat-message/chat-message-room.js +3 -1
  57. package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
  58. package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
  59. package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
  60. package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
  61. package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
  62. package/dist-server/service/chat-message/chat-typing.js +40 -0
  63. package/dist-server/service/chat-message/chat-typing.js.map +1 -0
  64. package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
  65. package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
  66. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
  67. package/dist-server/service/chat-session/attachment-tools.js +2 -2
  68. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  69. package/dist-server/service/chat-session/chat-session.d.ts +2 -1
  70. package/dist-server/service/chat-session/chat-session.js +1 -1
  71. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  72. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
  73. package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
  74. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
  75. package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
  76. package/dist-server/service/chat-session/session-activity-publish.js +5 -2
  77. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
  78. package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
  79. package/dist-server/service/chat-session/session-visibility.js +28 -3
  80. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  81. package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
  82. package/dist-server/service/chat-session/session-write-gate.js +1 -1
  83. package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
  84. package/dist-server/service/chat-session-participant/index.d.ts +1 -0
  85. package/dist-server/service/chat-session-participant/index.js +4 -1
  86. package/dist-server/service/chat-session-participant/index.js.map +1 -1
  87. package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
  88. package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
  89. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
  90. package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
  91. package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
  92. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
  93. package/dist-server/service/chat-session-participant/read-roster.js +5 -2
  94. package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
  95. package/dist-server/service/chat-session-resolver.d.ts +27 -0
  96. package/dist-server/service/chat-session-resolver.js +226 -25
  97. package/dist-server/service/chat-session-resolver.js.map +1 -1
  98. package/dist-server/service/data-entry/entry-target.d.ts +6 -0
  99. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  100. package/dist-server/service/data-entry/entry-tools.js +2 -2
  101. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  102. package/dist-server/service/data-entry/entry-update.js +1 -1
  103. package/dist-server/service/data-entry/entry-update.js.map +1 -1
  104. package/dist-server/service/data-read/list-read.d.ts +10 -0
  105. package/dist-server/service/data-read/list-read.js +12 -0
  106. package/dist-server/service/data-read/list-read.js.map +1 -1
  107. package/dist-server/service/data-read/read-target.d.ts +5 -0
  108. package/dist-server/service/data-read/read-target.js.map +1 -1
  109. package/dist-server/service/index.d.ts +4 -2
  110. package/dist-server/service/index.js +9 -1
  111. package/dist-server/service/index.js.map +1 -1
  112. package/dist-server/service/testing/conversation-script.d.ts +1 -1
  113. package/dist-server/service/testing/conversation-script.js.map +1 -1
  114. package/dist-server/service/testing/mode-conformance.js +2 -0
  115. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  116. package/dist-server/tsconfig.tsbuildinfo +1 -1
  117. package/package.json +2 -2
  118. package/server/service/assistant-chat-resolver.ts +7 -12
  119. package/server/service/assistant-mode/index.ts +1 -1
  120. package/server/service/assistant-mode/mode-categories.ts +11 -3
  121. package/server/service/assistant-mode/mode-front.test.ts +53 -1
  122. package/server/service/assistant-mode/screen-rules.test.ts +7 -1
  123. package/server/service/assistant-mode/screen-rules.ts +11 -2
  124. package/server/service/chat-message/chat-message-room.test.ts +7 -2
  125. package/server/service/chat-message/chat-message-room.ts +3 -1
  126. package/server/service/chat-message/chat-typing-subscription.ts +52 -0
  127. package/server/service/chat-message/chat-typing.test.ts +45 -0
  128. package/server/service/chat-message/chat-typing.ts +48 -0
  129. package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
  130. package/server/service/chat-session/attachment-tool-result.ts +7 -0
  131. package/server/service/chat-session/attachment-tools.ts +3 -3
  132. package/server/service/chat-session/chat-session.ts +3 -2
  133. package/server/service/chat-session/group-room.test.ts +233 -0
  134. package/server/service/chat-session/session-activity-broadcast.ts +20 -0
  135. package/server/service/chat-session/session-activity-publish.ts +6 -1
  136. package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
  137. package/server/service/chat-session/session-visibility.test.ts +32 -1
  138. package/server/service/chat-session/session-visibility.ts +33 -4
  139. package/server/service/chat-session/session-write-gate.test.ts +7 -1
  140. package/server/service/chat-session/session-write-gate.ts +2 -2
  141. package/server/service/chat-session-participant/index.ts +2 -0
  142. package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
  143. package/server/service/chat-session-participant/invite-notification.ts +43 -5
  144. package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
  145. package/server/service/chat-session-participant/mention-invite.ts +82 -28
  146. package/server/service/chat-session-participant/read-roster.test.ts +13 -4
  147. package/server/service/chat-session-participant/read-roster.ts +6 -3
  148. package/server/service/chat-session-resolver.ts +204 -27
  149. package/server/service/data-entry/data-entry-update.test.ts +8 -0
  150. package/server/service/data-entry/entry-target.ts +6 -0
  151. package/server/service/data-entry/entry-tools.ts +2 -2
  152. package/server/service/data-entry/entry-update.ts +1 -1
  153. package/server/service/data-read/list-missing.test.ts +49 -0
  154. package/server/service/data-read/list-read.ts +18 -0
  155. package/server/service/data-read/read-target.ts +5 -0
  156. package/server/service/index.ts +6 -1
  157. package/server/service/testing/conversation-script.ts +1 -1
  158. package/server/service/testing/mode-conformance.ts +1 -0
  159. package/test/assistant-dock.test.ts +5 -1
  160. package/test/assistant-session-manager.test.ts +4 -2
  161. package/test/open-doors.test.ts +10 -5
  162. package/translations/en.json +29 -4
  163. package/translations/es.json +29 -4
  164. package/translations/ja.json +29 -4
  165. package/translations/ko.json +29 -4
  166. package/translations/ms.json +29 -4
  167. package/translations/zh.json +29 -4
@@ -13,7 +13,8 @@ import {
13
13
  sessionActions,
14
14
  type AssistantSessionAdmin,
15
15
  type AssistantSessionEntry,
16
- type AssistantSessionFilters
16
+ type AssistantSessionFilters,
17
+ type SessionVisibility
17
18
  } from '../utils/assistant-session-admin.js'
18
19
 
19
20
  const PAGE = 30
@@ -34,7 +35,7 @@ const t = (key: string, defaultValue: string, options: Record<string, unknown> =
34
35
  * with the toolbar's own events and does not open anything itself:
35
36
  * assistant-session-select { sessionId } open that conversation
36
37
  * assistant-session-reset start a new one
37
- * assistant-session-changed { sessionId, change: 'renamed' | 'hidden' | 'unhidden' | 'removed' | 'visibility' }
38
+ * assistant-session-changed { sessionId, change: 'renamed' | 'hidden' | 'unhidden' | 'removed' | 'visibility' | 'left' }
38
39
  * a row changed - the host refreshes its tabs; if the open
39
40
  * conversation was removed or hidden, it moves off it
40
41
  *
@@ -459,7 +460,7 @@ export class AssistantSessionManager extends LitElement {
459
460
  void this.act(entry, hidden ? 'hidden' : 'unhidden', () => this.admin.hide(entry.id, hidden), t('hide-refused', 'this conversation could not be hidden.'))
460
461
  }
461
462
 
462
- private setVisibility(entry: AssistantSessionEntry, visibility: 'open' | 'private') {
463
+ private setVisibility(entry: AssistantSessionEntry, visibility: SessionVisibility) {
463
464
  void this.act(
464
465
  entry,
465
466
  'visibility',
@@ -497,8 +498,9 @@ export class AssistantSessionManager extends LitElement {
497
498
  </select>
498
499
  <select aria-label=${t('filter-visibility', 'Visibility')} .value=${this.visibility}
499
500
  @change=${(e: Event) => this.refilter(() => (this.visibility = (e.target as HTMLSelectElement).value as any))}>
500
- <option value="all">${t('filter-any-visibility', 'Public and private')}</option>
501
+ <option value="all">${t('filter-any-visibility', 'Anyone')}</option>
501
502
  <option value="open">${t('filter-open', 'Public')}</option>
503
+ <option value="group">${t('filter-group', 'Group')}</option>
502
504
  <option value="private">${t('filter-private', 'Private')}</option>
503
505
  </select>
504
506
  <label><input type="checkbox" .checked=${this.unreadOnly}
@@ -529,7 +531,7 @@ export class AssistantSessionManager extends LitElement {
529
531
  private renderRow(entry: AssistantSessionEntry): TemplateResult {
530
532
  const name = entry.name || t('untitled', 'Untitled conversation')
531
533
  const actions = sessionActions(entry)
532
- const isPrivate = entry.visibility === 'private'
534
+ const isOpen = !entry.visibility || entry.visibility === 'open'
533
535
 
534
536
  if (this.renamingId === entry.id) {
535
537
  return html`<div class="row">
@@ -548,13 +550,13 @@ export class AssistantSessionManager extends LitElement {
548
550
  }
549
551
 
550
552
  return html`<div
551
- class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${isPrivate ? '' : 'open-audience'}"
553
+ class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${isOpen ? 'open-audience' : ''}"
552
554
  >
553
555
  <span class="dot"></span>
554
556
  <button class="open-row" aria-current=${entry.id === this.sessionId ? 'true' : 'false'} @click=${() => this.emit('assistant-session-select', { sessionId: entry.id, kind: entry.kind })}>
555
557
  <div class="title">${name}</div>
556
558
  <div class="prev">${entry.lastMessagePreview || ''}</div>
557
- <span class="meta"><span class="audience ${isPrivate ? 'private' : 'open'}"><md-icon aria-hidden="true">${isPrivate ? 'lock' : 'public'}</md-icon>${isPrivate ? t('audience-private', 'Participants only') : t('audience-open', 'Everyone in this domain')}</span>${entry.lastMessageAt && Number.isFinite(Date.parse(entry.lastMessageAt)) ? html` · <time datetime=${entry.lastMessageAt}>${new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(entry.lastMessageAt))}</time>` : nothing}${entry.mine ? html` · ${t('filter-mine', 'Started by me')}` : nothing}</span>
559
+ <span class="meta">${this.renderAudience(entry.visibility)}${entry.lastMessageAt && Number.isFinite(Date.parse(entry.lastMessageAt)) ? html` · <time datetime=${entry.lastMessageAt}>${new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(entry.lastMessageAt))}</time>` : nothing}${entry.mine ? html` · ${t('filter-mine', 'Started by me')}` : nothing}</span>
558
560
  </button>
559
561
  <button
560
562
  class="more-menu"
@@ -571,16 +573,21 @@ export class AssistantSessionManager extends LitElement {
571
573
  private openRowMenu(event: MouseEvent, entry: AssistantSessionEntry, name: string) {
572
574
  event.stopPropagation()
573
575
  const actions = sessionActions(entry)
574
- const isPrivate = entry.visibility === 'private'
576
+ const current = entry.visibility || 'open'
575
577
  const at = (event.currentTarget as HTMLElement).getBoundingClientRect()
576
578
  OxPopupMenu.open({
577
579
  template: html`
578
580
  ${actions.rename ? html`<ox-popup-menuitem label=${t('rename', 'Rename')} @select=${() => (this.renamingId = entry.id)}></ox-popup-menuitem>` : nothing}
579
- ${actions.setVisibility
580
- ? isPrivate
581
- ? html`<ox-popup-menuitem label=${t('make-open', 'Make public')} @select=${() => this.askOpen(entry, name)}></ox-popup-menuitem>`
582
- : html`<ox-popup-menuitem label=${t('make-private', 'Make private')} @select=${() => this.setVisibility(entry, 'private')}></ox-popup-menuitem>`
581
+ ${actions.setVisibility && current !== 'open'
582
+ ? html`<ox-popup-menuitem label=${t('make-open', 'Make public')} @select=${() => this.askOpen(entry, name)}></ox-popup-menuitem>`
583
583
  : nothing}
584
+ ${actions.setVisibility && current !== 'group'
585
+ ? html`<ox-popup-menuitem label=${t('make-group', 'Make it a group')} @select=${() => this.setVisibility(entry, 'group')}></ox-popup-menuitem>`
586
+ : nothing}
587
+ ${actions.setVisibility && current !== 'private'
588
+ ? html`<ox-popup-menuitem label=${t('make-private', 'Make private')} @select=${() => this.askPrivate(entry, name)}></ox-popup-menuitem>`
589
+ : nothing}
590
+ ${actions.leave ? html`<ox-popup-menuitem label=${t('leave', 'Leave')} @select=${() => this.askLeave(entry, name)}></ox-popup-menuitem>` : nothing}
584
591
  <ox-popup-menuitem
585
592
  label=${actions.unhide ? t('unhide', 'Show in my list') : t('hide', 'Hide from my list')}
586
593
  @select=${() => this.setHidden(entry, !entry.hidden)}></ox-popup-menuitem>
@@ -620,4 +627,38 @@ export class AssistantSessionManager extends LitElement {
620
627
  )
621
628
  if (ok) this.setVisibility(entry, 'open')
622
629
  }
630
+
631
+ /* Private is the owner's alone - the people in it leave, and coming back is being invited again. */
632
+ private async askPrivate(entry: AssistantSessionEntry, name: string) {
633
+ const ok = await this.ask(
634
+ t('make-private', 'Make private'),
635
+ t('confirm-private', 'Make "{name}" private? Only you will read it, and everyone else in it leaves.', { name }),
636
+ t('make-private', 'Make private')
637
+ )
638
+ if (ok) this.setVisibility(entry, 'private')
639
+ }
640
+
641
+ private async askLeave(entry: AssistantSessionEntry, name: string) {
642
+ const ok = await this.ask(
643
+ t('leave', 'Leave'),
644
+ entry.visibility === 'open'
645
+ ? t('confirm-leave-open', 'Leave "{name}"? It stops coming to your list.', { name })
646
+ : t('confirm-leave', 'Leave "{name}"? You will not read it again unless its owner brings you back.', { name }),
647
+ t('leave', 'Leave')
648
+ )
649
+ if (ok) void this.act(entry, 'left', () => this.admin.leave(entry.id), t('leave-refused', 'you could not leave this conversation.'))
650
+ }
651
+
652
+ /** Who reads a conversation, on its row: the same three words wherever a conversation is listed. */
653
+ private renderAudience(visibility?: string) {
654
+ const v = visibility || 'open'
655
+ const icon = v === 'open' ? 'public' : v === 'group' ? 'group' : 'lock'
656
+ const label =
657
+ v === 'open'
658
+ ? t('audience-open', 'Everyone in this domain')
659
+ : v === 'group'
660
+ ? t('audience-group', 'Invited people')
661
+ : t('audience-private', 'Only me')
662
+ return html`<span class="audience ${v}"><md-icon aria-hidden="true">${icon}</md-icon>${label}</span>`
663
+ }
623
664
  }
@@ -50,3 +50,13 @@ it('★ a stored answer brings its cards back after a refresh', () => {
50
50
  it('the chat reads stored proposals from history and from the broadcast', () => {
51
51
  expect(chat.split('proposalsJson').length - 1).toBeGreaterThanOrEqual(2)
52
52
  })
53
+
54
+
55
+ it('a failed page-context handoff never sends the request into the previous conversation', () => {
56
+ const take = body(dock, 'private async takeAsk()')
57
+ expect(take.indexOf('if (!id) return')).toBeGreaterThan(take.indexOf('await this.startCarried(ask)'))
58
+ expect(take.indexOf('if (!id) return')).toBeLessThan(take.indexOf('this.asking ='))
59
+ const carry = body(dock, 'private async startCarried(')
60
+ expect(carry).toContain('if (created.errors?.length) throw')
61
+ expect(carry).toContain('if (attached.errors?.length) throw')
62
+ })
@@ -1,15 +1,5 @@
1
- import { rosterInviteState, rosterName } from './roster-identity'
2
- describe('roster identity and invite notice',()=>{
3
- it('does not claim denial when participant identities are missing or still loading',()=>{
4
- expect(rosterInviteState([{role:'owner'}],'me',true)).toBe('checking')
5
- expect(rosterInviteState([], 'me',false)).toBe('checking')
6
- expect(rosterInviteState([], undefined,true)).toBe('checking')
7
- })
8
- it('uses the current person role rather than another member or a stale loading state',()=>{
9
- for(const role of ['owner','member'])expect(rosterInviteState([{role,user:{id:'me'}}],'me',true)).toBe('allowed')
10
- expect(rosterInviteState([{role:'viewer',user:{id:'me'}},{role:'owner',user:{id:'other'}}],'me',true)).toBe('denied')
11
- expect(rosterInviteState([{role:'member',user:{id:'other'}}],'me',true)).toBe('denied')
12
- })
1
+ import { rosterName } from './roster-identity'
2
+ describe('roster identity',()=>{
13
3
  it('shows a recognizable full name, then username, then an explicit unavailable label',()=>{
14
4
  expect(rosterName({role:'member',user:{id:'u',name:' 정연희 '}},'Unavailable')).toBe('정연희')
15
5
  expect(rosterName({role:'member',user:{id:'u',username:'yeonhee'}},'Unavailable')).toBe('yeonhee')
@@ -1,10 +1,4 @@
1
1
  export interface RosterParticipant { role: string; user?: { id: string; name?: string; username?: string } }
2
- /** Missing identity/loading is not evidence that a person lacks membership. Server gates remain authoritative. */
3
- export function rosterInviteState(rows: readonly RosterParticipant[], userId: string | undefined, loaded: boolean): 'checking' | 'allowed' | 'denied' {
4
- if (!loaded || !userId || rows.some(row => !row.user?.id)) return 'checking'
5
- const role = rows.find(row => row.user?.id === userId)?.role
6
- return role === 'owner' || role === 'member' ? 'allowed' : 'denied'
7
- }
8
2
  export function rosterName(person: RosterParticipant, fallback: string): string {
9
3
  return person.user?.name?.trim() || person.user?.username?.trim() || fallback
10
4
  }
@@ -6,6 +6,7 @@
6
6
  * hide/unhide anyone, for their own list only (reversible)
7
7
  * delete the creator (`mine`) (not reversible from a screen)
8
8
  * visibility the owner (`owner`) (opening shows what is already written)
9
+ * leave a participant who is not the owner (the owner deletes or makes it private instead)
9
10
  */
10
11
  export function sessionActions(entry: { participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }) {
11
12
  return {
@@ -13,6 +14,7 @@ export function sessionActions(entry: { participant?: boolean; hidden?: boolean;
13
14
  hide: !entry.hidden,
14
15
  unhide: !!entry.hidden,
15
16
  remove: !!entry.mine,
16
- setVisibility: !!entry.owner
17
+ setVisibility: !!entry.owner,
18
+ leave: !!entry.participant && !entry.owner
17
19
  }
18
20
  }
@@ -17,9 +17,11 @@ export { sessionActions } from './assistant-session-actions.js'
17
17
  * hide/unhide anyone, for their own list only (reversible)
18
18
  * delete the creator (`mine`) (not reversible from a screen)
19
19
  * visibility the owner (`owner`) (opening shows what is already written)
20
+ * leave a member, never the owner (in a group, leaving ends reading here)
20
21
  */
21
22
 
22
- export type SessionVisibility = 'open' | 'private'
23
+ /** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */
24
+ export type SessionVisibility = 'open' | 'group' | 'private'
23
25
 
24
26
  /** One row of `chatSessionInbox`, as the list draws it. */
25
27
  export interface AssistantSessionEntry {
@@ -48,7 +50,7 @@ export interface AssistantSessionFilters {
48
50
  /** A kind (mode) to list; empty lists every kind on the anchor. */
49
51
  kind?: string
50
52
  whose?: 'all' | 'mine' | 'participant'
51
- visibility?: 'all' | 'open' | 'private'
53
+ visibility?: 'all' | SessionVisibility
52
54
  includeHidden?: boolean
53
55
  unreadOnly?: boolean
54
56
  }
@@ -77,6 +79,7 @@ export interface AssistantSessionAdmin {
77
79
  hide(sessionId: string, hidden: boolean): Promise<boolean>
78
80
  remove(sessionId: string): Promise<boolean>
79
81
  setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>
82
+ leave(sessionId: string): Promise<boolean>
80
83
  }
81
84
 
82
85
  /** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
@@ -163,5 +166,9 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
163
166
  { id: sessionId, v: visibility },
164
167
  'setSessionVisibility'
165
168
  )
169
+ },
170
+
171
+ leave(sessionId) {
172
+ return mutate(gql`mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession')
166
173
  }
167
174
  }
@@ -11,7 +11,7 @@ export interface AssistantSession {
11
11
  * 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.
12
12
  */
13
13
  kind?: string
14
- /** Who reads it: 'open' (everyone in the domain) or 'private' (participants). The dock draws an open one apart. */
14
+ /** Who reads it: 'open' (everyone in the domain), 'group' (the people the owner brought in) or 'private' (the owner alone). The dock draws an open one apart. */
15
15
  visibility?: string
16
16
  }
17
17
  export interface AssistantSessionTarget {
@@ -0,0 +1,33 @@
1
+ import { TYPING_SAY_EVERY_MS, TYPING_SHOW_MS, TypingRoom } from './assistant-typing'
2
+
3
+ describe('typing in a conversation', () => {
4
+ it('says it with words in the box, not on every key', () => {
5
+ const room = new TypingRoom()
6
+ expect(room.shouldSay('', 0)).toBe(false)
7
+ expect(room.shouldSay('h', 10_000)).toBe(true)
8
+ expect(room.shouldSay('he', 10_000 + TYPING_SAY_EVERY_MS - 1)).toBe(false)
9
+ expect(room.shouldSay('hel', 10_000 + TYPING_SAY_EVERY_MS)).toBe(true)
10
+ })
11
+
12
+ it('★ a notice fades without the next one', () => {
13
+ const room = new TypingRoom()
14
+ room.heard({ userId: 'u2', name: 'Kim' }, 0)
15
+ expect(room.typing(TYPING_SHOW_MS - 1)).toEqual(['Kim'])
16
+ expect(room.typing(TYPING_SHOW_MS)).toEqual([])
17
+ })
18
+
19
+ it('★ the message ends it at once', () => {
20
+ const room = new TypingRoom()
21
+ room.heard({ userId: 'u2', name: 'Kim' }, 0)
22
+ room.heard({ userId: 'u3', name: 'Lee' }, 0)
23
+ expect(room.spoke('u2')).toBe(true)
24
+ expect(room.typing(1)).toEqual(['Lee'])
25
+ })
26
+
27
+ it('a new notice from the same person keeps one line for them', () => {
28
+ const room = new TypingRoom()
29
+ room.heard({ userId: 'u2', name: 'Kim' }, 0)
30
+ room.heard({ userId: 'u2', name: 'Kim' }, TYPING_SHOW_MS - 1)
31
+ expect(room.typing(TYPING_SHOW_MS + 1)).toEqual(['Kim'])
32
+ })
33
+ })
@@ -0,0 +1,53 @@
1
+ /*
2
+ * "Someone is typing" on the client - when to say it, and whose to show. Pure, so it is tested alone
3
+ * (`assistant-typing.test.ts`); the chat sends and receives, this keeps the times.
4
+ *
5
+ * Nothing is stored on the server, so a notice stands only until the next would have come: the typist says it
6
+ * every few seconds while the words change, the room lets it fade a little after the last one, and a message
7
+ * from that person ends it at once - the words arrived.
8
+ */
9
+
10
+ /** How often a typist says it while typing. */
11
+ export const TYPING_SAY_EVERY_MS = 3_000
12
+
13
+ /** How long a notice stands without the next - the server's `TYPING_NOTICE_MS`. */
14
+ export const TYPING_SHOW_MS = 6_000
15
+
16
+ export interface TypingNotice {
17
+ userId: string
18
+ name: string
19
+ }
20
+
21
+ export class TypingRoom {
22
+ private saidAt = 0
23
+ private heardFrom = new Map<string, { name: string; until: number }>()
24
+
25
+ /** Should the typist say it now? Only with words in the box, and not more often than every few seconds. */
26
+ shouldSay(text: string, now = Date.now()): boolean {
27
+ if (!text.trim()) return false
28
+ if (now - this.saidAt < TYPING_SAY_EVERY_MS) return false
29
+ this.saidAt = now
30
+ return true
31
+ }
32
+
33
+ heard(notice: TypingNotice, now = Date.now()): void {
34
+ if (!notice?.userId) return
35
+ this.heardFrom.set(notice.userId, { name: notice.name, until: now + TYPING_SHOW_MS })
36
+ }
37
+
38
+ /** That person's message arrived - they are not typing it any more. */
39
+ spoke(userId: string | undefined): boolean {
40
+ return !!userId && this.heardFrom.delete(userId)
41
+ }
42
+
43
+ /** The names typing now, in the order they started; faded ones are dropped. */
44
+ typing(now = Date.now()): string[] {
45
+ for (const [id, one] of this.heardFrom) if (one.until <= now) this.heardFrom.delete(id)
46
+ return [...this.heardFrom.values()].map(one => one.name)
47
+ }
48
+
49
+ clear(): void {
50
+ this.saidAt = 0
51
+ this.heardFrom.clear()
52
+ }
53
+ }
@@ -1,3 +1,4 @@
1
+ import './assistant-invite.js';
1
2
  /**
2
3
  * <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
3
4
  *
@@ -41,6 +42,8 @@ export declare class OxAssistantChat extends LitElement {
41
42
  /** 영속 세션 id. 없으면 ad-hoc (메시지 영속 안 됨). */
42
43
  sessionId?: string;
43
44
  prepareSession?: (adopt: (id: string) => void) => Promise<string | undefined>;
45
+ /** Who reads this conversation - 'open' · 'group' · 'private'. The host passes what its list says; the roster follows it. */
46
+ visibility?: string;
44
47
  private adoptingSession;
45
48
  /**
46
49
  * 보드의 모든 ChatSession 리스트 — 다중 세션 활성화 시 탭 UI 로 표시.
@@ -310,6 +313,15 @@ export declare class OxAssistantChat extends LitElement {
310
313
  /** C4 presence/roster — 세션 참여자 목록. lastSeenAt 으로 online 판정. */
311
314
  private participants;
312
315
  private rosterLoaded;
316
+ /** May I bring someone in here - the server's answer (`mayInviteToAISession`); unknown until the roster is read. */
317
+ private mayInvite?;
318
+ /** The name box under the roster is open. */
319
+ private inviting;
320
+ /** Who is typing now, by name - kept by `typingRoom`, redrawn on each notice and as they fade. */
321
+ private typingNames;
322
+ private typingRoom;
323
+ private _typingSub?;
324
+ private _typingTimer?;
313
325
  /** roster online 재평가 트리거 — heartbeat tick 마다 갱신(시간 경과 반영). */
314
326
  private _presenceTick;
315
327
  private _heartbeatTimer?;
@@ -390,6 +402,9 @@ export declare class OxAssistantChat extends LitElement {
390
402
  private _startPresence;
391
403
  private _stopPresence;
392
404
  private _startMessageSubscription;
405
+ /** The room's "typing" line - notices of the others, which fade by themselves (`TypingRoom`). */
406
+ private _startTypingSubscription;
407
+ private _refreshTyping;
393
408
  private _stopMessageSubscription;
394
409
  /** 서버 ChatMessage → ChatLine. */
395
410
  private _msgToLine;
@@ -494,7 +509,20 @@ export declare class OxAssistantChat extends LitElement {
494
509
  private renderAgentPicker;
495
510
  private renderSessionTabs;
496
511
  /** C4 roster — 세션 참여자 + online 표시. 참여자 2명 미만이면 숨김(혼자면 의미 없음). */
512
+ /**
513
+ * Who is in the room. A group shows it always - its people are the point of it, and the owner brings them in
514
+ * here; any other room once someone else has joined. The buttons follow what the server said this person may
515
+ * do (`mayInviteToAISession`, the owner's role); the doors decide again.
516
+ */
497
517
  private renderRoster;
518
+ /** Bring someone in - the door seats them and tells them (`inviteToAISession`). */
519
+ private _invite;
520
+ /** Send a member out - in a group they no longer read or write here. Bringing them back is inviting them again. */
521
+ private _removeParticipant;
522
+ /** Tell the others I am typing - only where someone else could hear it, and not on every key (`TypingRoom`). */
523
+ private _sayTyping;
524
+ /** "Kim is typing…" - over the box, while their notices keep coming. */
525
+ private renderTyping;
498
526
  private _onSessionTabClick;
499
527
  private _onSessionCreateClick;
500
528
  private renderEmpty;