@things-factory/ai-assistant 10.1.210 → 10.1.213

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 (119) hide show
  1. package/client/components/assistant-chat.ts +115 -2
  2. package/client/components/assistant-dock.ts +106 -3
  3. package/client/components/workspace-view.ts +56 -3
  4. package/client/index.ts +3 -0
  5. package/client/utils/assistant-ask.test.ts +38 -0
  6. package/client/utils/assistant-ask.ts +35 -1
  7. package/client/utils/assistant-proposals.ts +23 -0
  8. package/client/utils/assistant-selection.test.ts +30 -0
  9. package/client/utils/assistant-selection.ts +47 -0
  10. package/client/utils/assistant-session-controller.ts +2 -0
  11. package/client/utils/assistant-session-transport.ts +1 -1
  12. package/client/utils/invite-proposal-register.ts +31 -0
  13. package/client/utils/invite-proposal.test.ts +40 -0
  14. package/client/utils/invite-proposal.ts +54 -0
  15. package/dist-client/components/assistant-chat.d.ts +18 -0
  16. package/dist-client/components/assistant-chat.js +117 -2
  17. package/dist-client/components/assistant-chat.js.map +1 -1
  18. package/dist-client/components/assistant-dock.d.ts +11 -0
  19. package/dist-client/components/assistant-dock.js +97 -4
  20. package/dist-client/components/assistant-dock.js.map +1 -1
  21. package/dist-client/components/workspace-view.d.ts +2 -0
  22. package/dist-client/components/workspace-view.js +58 -3
  23. package/dist-client/components/workspace-view.js.map +1 -1
  24. package/dist-client/index.d.ts +2 -0
  25. package/dist-client/index.js +3 -0
  26. package/dist-client/index.js.map +1 -1
  27. package/dist-client/tsconfig.tsbuildinfo +1 -1
  28. package/dist-client/utils/assistant-ask.d.ts +22 -1
  29. package/dist-client/utils/assistant-ask.js +19 -0
  30. package/dist-client/utils/assistant-ask.js.map +1 -1
  31. package/dist-client/utils/assistant-ask.test.d.ts +1 -0
  32. package/dist-client/utils/assistant-ask.test.js +40 -0
  33. package/dist-client/utils/assistant-ask.test.js.map +1 -0
  34. package/dist-client/utils/assistant-proposals.d.ts +14 -0
  35. package/dist-client/utils/assistant-proposals.js +7 -0
  36. package/dist-client/utils/assistant-proposals.js.map +1 -1
  37. package/dist-client/utils/assistant-selection.d.ts +19 -0
  38. package/dist-client/utils/assistant-selection.js +35 -0
  39. package/dist-client/utils/assistant-selection.js.map +1 -0
  40. package/dist-client/utils/assistant-selection.test.d.ts +1 -0
  41. package/dist-client/utils/assistant-selection.test.js +26 -0
  42. package/dist-client/utils/assistant-selection.test.js.map +1 -0
  43. package/dist-client/utils/assistant-session-controller.d.ts +2 -0
  44. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  45. package/dist-client/utils/assistant-session-transport.js +1 -1
  46. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  47. package/dist-client/utils/invite-proposal-register.d.ts +1 -0
  48. package/dist-client/utils/invite-proposal-register.js +24 -0
  49. package/dist-client/utils/invite-proposal-register.js.map +1 -0
  50. package/dist-client/utils/invite-proposal.d.ts +17 -0
  51. package/dist-client/utils/invite-proposal.js +37 -0
  52. package/dist-client/utils/invite-proposal.js.map +1 -0
  53. package/dist-client/utils/invite-proposal.test.d.ts +1 -0
  54. package/dist-client/utils/invite-proposal.test.js +35 -0
  55. package/dist-client/utils/invite-proposal.test.js.map +1 -0
  56. package/dist-server/service/assistant-chat-resolver.js +2 -0
  57. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  58. package/dist-server/service/assistant-mode/index.d.ts +2 -1
  59. package/dist-server/service/assistant-mode/index.js +6 -1
  60. package/dist-server/service/assistant-mode/index.js.map +1 -1
  61. package/dist-server/service/assistant-mode/mode-categories.d.ts +14 -1
  62. package/dist-server/service/assistant-mode/mode-categories.js +29 -4
  63. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  64. package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
  65. package/dist-server/service/assistant-mode/screen-rules.js +21 -6
  66. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
  67. package/dist-server/service/data-read/read-target.d.ts +24 -3
  68. package/dist-server/service/data-read/read-target.js +42 -10
  69. package/dist-server/service/data-read/read-target.js.map +1 -1
  70. package/dist-server/service/data-read/read-tools.d.ts +18 -1
  71. package/dist-server/service/data-read/read-tools.js +19 -5
  72. package/dist-server/service/data-read/read-tools.js.map +1 -1
  73. package/dist-server/service/index.d.ts +5 -1
  74. package/dist-server/service/index.js +10 -1
  75. package/dist-server/service/index.js.map +1 -1
  76. package/dist-server/service/people/index.d.ts +4 -0
  77. package/dist-server/service/people/index.js +22 -0
  78. package/dist-server/service/people/index.js.map +1 -0
  79. package/dist-server/service/people/invite-tools.d.ts +34 -0
  80. package/dist-server/service/people/invite-tools.js +144 -0
  81. package/dist-server/service/people/invite-tools.js.map +1 -0
  82. package/dist-server/service/people/people-keys.d.ts +8 -0
  83. package/dist-server/service/people/people-keys.js +16 -0
  84. package/dist-server/service/people/people-keys.js.map +1 -0
  85. package/dist-server/service/people/people-read.d.ts +4 -0
  86. package/dist-server/service/people/people-read.js +57 -0
  87. package/dist-server/service/people/people-read.js.map +1 -0
  88. package/dist-server/service/testing/mode-conformance.js +3 -0
  89. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  90. package/dist-server/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +6 -6
  92. package/server/service/assistant-chat-resolver.ts +2 -0
  93. package/server/service/assistant-mode/conversation-category.test.ts +35 -0
  94. package/server/service/assistant-mode/index.ts +3 -1
  95. package/server/service/assistant-mode/mode-categories.ts +41 -4
  96. package/server/service/assistant-mode/mode-front.test.ts +5 -3
  97. package/server/service/assistant-mode/resolve.test.ts +1 -1
  98. package/server/service/assistant-mode/screen-rules.test.ts +23 -1
  99. package/server/service/assistant-mode/screen-rules.ts +22 -5
  100. package/server/service/data-entry/data-entry.test.ts +2 -2
  101. package/server/service/data-read/detail-lists.test.ts +61 -0
  102. package/server/service/data-read/read-target.ts +54 -9
  103. package/server/service/data-read/read-tools.ts +32 -6
  104. package/server/service/index.ts +7 -1
  105. package/server/service/people/index.ts +13 -0
  106. package/server/service/people/invite-tools.ts +159 -0
  107. package/server/service/people/invite.test.ts +159 -0
  108. package/server/service/people/people-keys.ts +16 -0
  109. package/server/service/people/people-read.test.ts +82 -0
  110. package/server/service/people/people-read.ts +56 -0
  111. package/server/service/testing/mode-conformance.test.ts +14 -1
  112. package/server/service/testing/mode-conformance.ts +2 -0
  113. package/test/assistant-dock.test.ts +73 -0
  114. package/translations/en.json +18 -1
  115. package/translations/es.json +18 -1
  116. package/translations/ja.json +18 -1
  117. package/translations/ko.json +19 -2
  118. package/translations/ms.json +18 -1
  119. package/translations/zh.json +18 -1
@@ -20,7 +20,7 @@ export const assistantSessionTransport: AssistantSessionTransport = {
20
20
  * Every readable conversation of this kind on the anchor, not only mine - the same scope the manager
21
21
  * shows by default (design/ai/shared-dock.md §2-6). Without a kind: every kind, as before.
22
22
  */
23
- query: gql`query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind visibility}}`,
23
+ query: gql`query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind visibility participant}}`,
24
24
  variables: { type: target.anchorType, id: target.anchorId, kind: target.kind || null }, fetchPolicy: 'no-cache'
25
25
  })
26
26
  if (result.errors?.length) throw new Error(result.errors[0].message)
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Pressing an invite card seats the person through the invite door, as the one who pressed (ADR-0073 decision 3).
3
+ * The card is ai-assistant's own tool (server: people/invite-tools.ts), so its handler is registered here, by the
4
+ * package that declared it - the dock knows no proposal kind (design/ai/shared-dock.md §4).
5
+ */
6
+ import gql from 'graphql-tag'
7
+
8
+ import { i18next } from '@operato/i18n'
9
+ import { client } from '@operato/graphql'
10
+
11
+ import { registerProposalHandler } from './assistant-proposals.js'
12
+ import { inviteProposalHandler } from './invite-proposal.js'
13
+
14
+ registerProposalHandler(
15
+ 'proposeInvite',
16
+ inviteProposalHandler(
17
+ async (sessionId, userIds) => {
18
+ const result = await client.mutate({
19
+ mutation: gql`
20
+ mutation ($id: String!, $users: [String!]!) {
21
+ inviteToAISession(sessionId: $id, userIds: $users)
22
+ }
23
+ `,
24
+ variables: { id: sessionId, users: userIds }
25
+ })
26
+ if (result.errors?.length) throw new Error(result.errors[0].message)
27
+ return result.data?.inviteToAISession ?? []
28
+ },
29
+ (key, options) => String(i18next.t(key, options))
30
+ )
31
+ )
@@ -0,0 +1,40 @@
1
+ import { inviteProposalHandler, invitedBy, isInviteProposal } from './invite-proposal'
2
+
3
+ const card = (people: any[], extra: any = {}) => ({ tool: 'proposeInvite', kind: 'invite', people, ...extra })
4
+ const kim = { userId: 'u-1', name: '정연희', department: 'Sales' }
5
+ const lee = { userId: 'u-2', name: '정연희', department: 'Finance' }
6
+ const t = (_key: string, options: any) => String(options.defaultValue).replace('{names}', options.names)
7
+ const context = (sessionId?: string) => ({ sessionId, confirm: async () => true, notify: () => {} }) as any
8
+
9
+ describe('the invite card', () => {
10
+ it('is the invite tool\'s card with someone on it', () => {
11
+ expect(isInviteProposal(card([kim]))).toBe(true)
12
+ expect(isInviteProposal(card([]))).toBe(false)
13
+ expect(isInviteProposal({ ...card([kim]), tool: 'proposeAction' })).toBe(false)
14
+ })
15
+
16
+ it('★ one person: pressing invites them; several: only the one picked - nothing is guessed', () => {
17
+ expect(invitedBy(card([kim]))).toEqual([kim])
18
+ expect(invitedBy(card([kim, lee]))).toEqual([])
19
+ expect(invitedBy(card([kim, lee], { chosen: 'u-2' }))).toEqual([lee])
20
+ })
21
+
22
+ it('★ pressing calls the invite door with the conversation and the person picked', async () => {
23
+ const calls: any[] = []
24
+ const handler = inviteProposalHandler(async (sessionId, userIds) => (calls.push([sessionId, userIds]), userIds), t)
25
+ expect(await handler(card([kim, lee], { chosen: 'u-2' }), context('s1'))).toMatchObject({ verdict: 'accepted', applied: 1, note: '정연희 joined this conversation and was told.' })
26
+ expect(calls).toEqual([['s1', ['u-2']]])
27
+ })
28
+
29
+ it('no pick, no conversation, or nobody seated - said, never accepted', async () => {
30
+ const handler = inviteProposalHandler(async (_s, userIds) => userIds, t)
31
+ expect(await handler(card([kim, lee]), context('s1'))).toEqual({ verdict: 'refused', errorCode: 'no-person-chosen' })
32
+ expect(await handler(card([kim]), context())).toEqual({ verdict: 'unavailable', errorCode: 'no-conversation' })
33
+ expect(await inviteProposalHandler(async () => [], t)(card([kim]), context('s1'))).toEqual({ verdict: 'refused', errorCode: 'already-here' })
34
+ })
35
+
36
+ it("the door's refusal is not swallowed - it is thrown to the dock, which reports it", async () => {
37
+ const handler = inviteProposalHandler(async () => { throw new Error('only the person who started this group conversation can bring people in.') }, t)
38
+ await expect(handler(card([kim]), context('s1'))).rejects.toThrow(/only the person who started/)
39
+ })
40
+ })
@@ -0,0 +1,54 @@
1
+ /*
2
+ * The invite card (server: people/invite-tools.ts) - who it offers, and what pressing it does.
3
+ *
4
+ * Pressing is the person's act: the dock calls `inviteToAISession` with their own request, and the door decides
5
+ * whether they may seat anyone here (`maySeat`). The model's card seats nobody by itself. A card with several people
6
+ * of one name is pressed on one of them - the press carries `chosen`; nothing is guessed.
7
+ */
8
+ import type { ProposalHandler } from './assistant-proposals.js'
9
+
10
+ export interface InvitePerson {
11
+ userId: string
12
+ name: string
13
+ department?: string
14
+ position?: string
15
+ }
16
+
17
+ export function isInviteProposal(proposal: any): boolean {
18
+ return proposal?.kind === 'invite' && proposal?.tool === 'proposeInvite' && Array.isArray(proposal?.people) && proposal.people.length > 0
19
+ }
20
+
21
+ /** The people a press brings in: the one picked, or the only one on the card. */
22
+ export function invitedBy(proposal: any): InvitePerson[] {
23
+ const people: InvitePerson[] = Array.isArray(proposal?.people) ? proposal.people.filter((one: any) => one?.userId) : []
24
+ if (proposal?.chosen) return people.filter(one => one.userId === proposal.chosen)
25
+ return people.length === 1 ? people : []
26
+ }
27
+
28
+ /** One line under a person's name - where they work. */
29
+ export function personLine(person: InvitePerson): string {
30
+ return [person.department, person.position].filter(Boolean).join(' · ')
31
+ }
32
+
33
+ /**
34
+ * Presses the card through the invite door. `invite` is the door - `inviteToAISession(sessionId, userIds)`, answering the
35
+ * ids seated now; it throws the door's refusal, which the dock reports as `refused` with that message.
36
+ */
37
+ export function inviteProposalHandler(
38
+ invite: (sessionId: string, userIds: string[]) => Promise<string[]>,
39
+ t: (key: string, options: Record<string, unknown>) => string
40
+ ): ProposalHandler {
41
+ return async (proposal, context) => {
42
+ const people = invitedBy(proposal)
43
+ if (!people.length) return { verdict: 'refused', errorCode: 'no-person-chosen' }
44
+ if (!context.sessionId) return { verdict: 'unavailable', errorCode: 'no-conversation' }
45
+ const seated = await invite(context.sessionId, people.map(one => one.userId))
46
+ if (!seated.length) return { verdict: 'refused', errorCode: 'already-here' }
47
+ const names = people.filter(one => seated.includes(one.userId)).map(one => one.name).join(', ')
48
+ return {
49
+ verdict: 'accepted',
50
+ applied: seated.length,
51
+ note: t('ai-assistant.people.invited', { names, defaultValue: '{names} joined this conversation and was told.' })
52
+ }
53
+ }
54
+ }
@@ -309,6 +309,15 @@ export declare class OxAssistantChat extends LitElement {
309
309
  text: string;
310
310
  attachments: ChatAttachment[];
311
311
  }): void;
312
+ /**
313
+ * A request made on the screen (「AI와 작업」, design/ai/shared-dock.md §2-11-1) - in this conversation, made now if it
314
+ * has none yet: the selection's note first, then the request, sent at once. What the person was writing is set aside
315
+ * while it goes and put back after, whatever happened. Says whether it was sent and, if not, why.
316
+ */
317
+ sendScreenRequest(text: string, note: string): Promise<{
318
+ sent: boolean;
319
+ reason?: string;
320
+ }>;
312
321
  private errorMessage?;
313
322
  /** C4 presence/roster — 세션 참여자 목록. lastSeenAt 으로 online 판정. */
314
323
  private participants;
@@ -560,6 +569,15 @@ export declare class OxAssistantChat extends LitElement {
560
569
  * 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
561
570
  */
562
571
  private renderProposals;
572
+ /** A package's preview under its card - the element is the package's; it is handed the proposal and where it is. */
573
+ private renderProposalPreview;
574
+ /** One preview element per card, kept across renders so it does not read again on every keystroke. */
575
+ private proposalPreviews;
576
+ /**
577
+ * The invite card. Pressing it is the person's act - the dock calls the invite door with their request. With several
578
+ * people of one name each is a button with where they work; the press carries whom they picked.
579
+ */
580
+ private renderInviteCard;
563
581
  /** The screen card - which list, the conditions the screen does not take, and the way to open it. */
564
582
  private renderOpenPageCard;
565
583
  /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
@@ -5,6 +5,8 @@ import { rosterName } from './roster-identity';
5
5
  import { notify } from '@operato/layout';
6
6
  import './assistant-invite.js';
7
7
  import { TypingRoom } from '../utils/assistant-typing.js';
8
+ import { isInviteProposal, personLine } from '../utils/invite-proposal.js';
9
+ import { noteForDisplay } from '../utils/assistant-selection.js';
8
10
  /**
9
11
  * <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
10
12
  *
@@ -63,6 +65,7 @@ import './entry-review.js';
63
65
  import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
64
66
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
65
67
  import { isOpenPageProposal, openPageNote } from '../utils/open-page.js';
68
+ import { proposalViewOf } from '../utils/assistant-proposals.js';
66
69
  function formatFileSize(bytes) {
67
70
  if (!bytes || bytes < 1024)
68
71
  return `${bytes || 0} B`;
@@ -279,6 +282,12 @@ const CHAT_TYPING_SUBSCRIPTION = gql `
279
282
  }
280
283
  }
281
284
  `;
285
+ /* The selection a screen request carries, noted before the request (design/ai/shared-dock.md §2-11-1). */
286
+ const APPEND_SESSION_NOTE_MUTATION = gql `
287
+ mutation AppendSessionNote($sessionId: String!, $content: String!) {
288
+ appendSessionNote(sessionId: $sessionId, content: $content)
289
+ }
290
+ `;
282
291
  const TOUCH_PRESENCE_MUTATION = gql `
283
292
  mutation TouchPresence($sessionId: String!) {
284
293
  touchPresence(sessionId: $sessionId)
@@ -442,6 +451,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
442
451
  this.foldNoticeDismissed = true;
443
452
  this.dispatchEvent(new CustomEvent('assistant-start-new-session', { bubbles: true, composed: true }));
444
453
  };
454
+ /** One preview element per card, kept across renders so it does not read again on every keystroke. */
455
+ this.proposalPreviews = new Map();
445
456
  /**
446
457
  * 실행은 호스트가 한다 — 이 컴포넌트는 도메인(트윈 명령 채널)을 모른다.
447
458
  * 확인 대화·명령 전송·결과 기록은 호스트의 몫이고, 여기서는 중복 클릭만 막는다.
@@ -715,6 +726,47 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
715
726
  this.input = draft.text;
716
727
  this.attachments = draft.attachments;
717
728
  }
729
+ /**
730
+ * A request made on the screen (「AI와 작업」, design/ai/shared-dock.md §2-11-1) - in this conversation, made now if it
731
+ * has none yet: the selection's note first, then the request, sent at once. What the person was writing is set aside
732
+ * while it goes and put back after, whatever happened. Says whether it was sent and, if not, why.
733
+ */
734
+ async sendScreenRequest(text, note) {
735
+ if (this.busy)
736
+ return { sent: false, reason: i18next.t('ai-assistant.text.screen-request-busy', { defaultValue: 'an answer is still being written.' }) };
737
+ const kept = this.takeDraft();
738
+ try {
739
+ if (!this.sessionId && this.prepareSession) {
740
+ await this.prepareSession(id => {
741
+ this.adoptingSession = true;
742
+ this.sessionId = id;
743
+ });
744
+ await this.updateComplete;
745
+ }
746
+ if (!this.sessionId)
747
+ return { sent: false, reason: i18next.t('ai-assistant.text.screen-request-no-session', { defaultValue: 'this screen has no conversation to send to.' }) };
748
+ if (note) {
749
+ const noted = await client.mutate({ mutation: APPEND_SESSION_NOTE_MUTATION, variables: { sessionId: this.sessionId, content: note } });
750
+ if (noted.errors?.length)
751
+ throw new Error(noted.errors[0].message);
752
+ await this.loadHistory();
753
+ }
754
+ this.errorMessage = undefined;
755
+ this.input = text;
756
+ await this.send();
757
+ return this.errorMessage ? { sent: false, reason: this.errorMessage } : { sent: true };
758
+ }
759
+ catch (error) {
760
+ return { sent: false, reason: error instanceof Error ? error.message : String(error) };
761
+ }
762
+ finally {
763
+ /* The request is the screen's - it never stays in the box; the box gets back what the person had written. */
764
+ if (this.input.trim() === text.trim())
765
+ this.input = '';
766
+ if (kept && !this.input.trim() && !this.attachments.length)
767
+ this.restoreDraft(kept);
768
+ }
769
+ }
718
770
  /** 현재 로그인 사용자 email — 그룹챗에서 내 메시지 vs 타인 메시지 구분.
719
771
  * email 을 쓰는 이유: /auth/profile 이 항상 내려주는 로그인 식별자이고(비어 있지 않다),
720
772
  * 내부 관리키(User.id)를 클라이언트 판정에 의존하지 않기 위함. */
@@ -1664,6 +1716,16 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
1664
1716
  --md-icon-size: 13px;
1665
1717
  color: inherit;
1666
1718
  }
1719
+ /* The invite card's people - where each works, quieter than the name; a picked card greys every button. */
1720
+ .proposal-choices .choice-chip .invite-where {
1721
+ margin-left: 4px;
1722
+ opacity: 0.7;
1723
+ font-size: 0.92em;
1724
+ }
1725
+ .proposal-choices .choice-chip:disabled {
1726
+ opacity: 0.5;
1727
+ cursor: default;
1728
+ }
1667
1729
 
1668
1730
  /* The entry card's way to the large review - a quiet action under its table, never a second run button. */
1669
1731
  .workspace-review {
@@ -2924,7 +2986,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2924
2986
  class="sys-text"
2925
2987
  title=${i18next.t('ai-assistant.text.toggle-system-note', { defaultValue: 'click to expand' })}
2926
2988
  @click=${() => this.toggleSystemNote(idx)}
2927
- >${content}</span
2989
+ >${noteForDisplay(content)}</span
2928
2990
  >`
2929
2991
  : content}
2930
2992
  ${line.attachments && line.attachments.length > 0
@@ -4022,7 +4084,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4022
4084
  if (isOpenPageProposal(p))
4023
4085
  return this.renderOpenPageCard(p);
4024
4086
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) });
4087
+ /* An invite card: one person, one button; several of one name, one button each - the person picks (ADR-0073 decision 3). */
4088
+ if (isInviteProposal(p))
4089
+ return this.renderInviteCard(p, key, card.state);
4025
4090
  const entry = isEntryProposal(p);
4091
+ /* A package's own card - its preview under the line, its words on the button (assistant-proposals.ts ProposalView). */
4092
+ const view = entry ? undefined : proposalViewOf(p.tool);
4026
4093
  const replaced = entry && latestEntry?.get(entryGroupOf(p)) !== key && card.state === 'ready';
4027
4094
  const ready = entry ? entryReadyCount(p) : 0;
4028
4095
  const sent = card.state !== 'ready' || replaced || (entry && ready === 0);
@@ -4068,7 +4135,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4068
4135
  ? i18next.t('ai-assistant.entry.change', { defaultValue: 'change {count} records', count: ready })
4069
4136
  : i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
4070
4137
  : i18next.t('ai-assistant.entry.nothing-to-enter', { defaultValue: 'nothing to enter' })
4071
- : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
4138
+ : view?.runLabelKey || view?.runLabel
4139
+ ? i18next.t(view.runLabelKey ?? '', { defaultValue: view.runLabel ?? 'run' })
4140
+ : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
4072
4141
  </button>
4073
4142
  `}
4074
4143
  ${ /*
@@ -4085,6 +4154,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4085
4154
  ` : nothing}
4086
4155
  </div>
4087
4156
  ${entry ? html `<ox-assistant-entry-review .proposal=${p}></ox-assistant-entry-review>` : nothing}
4157
+ ${view?.previewTag ? this.renderProposalPreview(view.previewTag, p, key) : nothing}
4088
4158
  ${entry && this.sessionId && offersWorkspaceReview(p)
4089
4159
  ? html `<button
4090
4160
  class="workspace-review"
@@ -4112,6 +4182,51 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4112
4182
  </div>
4113
4183
  `;
4114
4184
  }
4185
+ /** A package's preview under its card - the element is the package's; it is handed the proposal and where it is. */
4186
+ renderProposalPreview(tag, p, key) {
4187
+ const element = this.proposalPreviews.get(key) ?? document.createElement(tag);
4188
+ this.proposalPreviews.set(key, element);
4189
+ element.proposal = p;
4190
+ element.sessionId = this.sessionId;
4191
+ element.proposalKey = key;
4192
+ return html `${element}`;
4193
+ }
4194
+ /**
4195
+ * The invite card. Pressing it is the person's act - the dock calls the invite door with their request. With several
4196
+ * people of one name each is a button with where they work; the press carries whom they picked.
4197
+ */
4198
+ renderInviteCard(p, key, state) {
4199
+ const people = p.people;
4200
+ const sent = state !== 'ready';
4201
+ const status = sent ? this.proposalStateText(key) : '';
4202
+ return html `
4203
+ <div class="proposal invite ${sent ? 'sent' : ''}">
4204
+ <md-icon>${p.icon || 'person_add'}</md-icon>
4205
+ <span class="pbody">
4206
+ <span class="plabel">${p.label}</span>
4207
+ ${p.reason ? html `<span class="preason">${p.reason}</span>` : nothing}
4208
+ ${sent && people.length > 1 ? html `<span class="preason">${status}</span>` : nothing}
4209
+ </span>
4210
+ ${people.length === 1
4211
+ ? html `<button class="run" ?disabled=${sent} @click=${() => this.executeProposal(p, key)}>
4212
+ ${sent ? status : i18next.t('ai-assistant.button.invite', { defaultValue: 'Invite' })}
4213
+ </button>`
4214
+ : nothing}
4215
+ </div>
4216
+ ${people.length > 1
4217
+ ? html `<div class="proposal-choices invite-people" role="group" aria-label=${p.label}>
4218
+ ${people.map(person => html `<button
4219
+ class="choice-chip"
4220
+ ?disabled=${sent}
4221
+ title=${personLine(person)}
4222
+ @click=${() => this.executeProposal({ ...p, chosen: person.userId }, key)}>
4223
+ <md-icon>person_add</md-icon>
4224
+ <span>${person.name}${personLine(person) ? html ` <span class="invite-where">${personLine(person)}</span>` : nothing}</span>
4225
+ </button>`)}
4226
+ </div>`
4227
+ : nothing}
4228
+ `;
4229
+ }
4115
4230
  /** The screen card - which list, the conditions the screen does not take, and the way to open it. */
4116
4231
  renderOpenPageCard(p) {
4117
4232
  const note = openPageNote(p, (key, options) => String(i18next.t(key, options)));