@things-factory/ai-assistant 10.1.123 → 10.1.127

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 (42) hide show
  1. package/client/components/assistant-chat.ts +145 -95
  2. package/client/components/chat-echo-dedup.ts +2 -1
  3. package/client/components/reply-message.test.ts +15 -0
  4. package/client/components/reply-message.ts +11 -0
  5. package/client/components/roster-identity.test.ts +18 -0
  6. package/client/components/roster-identity.ts +10 -0
  7. package/client/utils/assistant-navigation.test.ts +22 -0
  8. package/dist-client/components/assistant-chat.d.ts +7 -0
  9. package/dist-client/components/assistant-chat.js +148 -94
  10. package/dist-client/components/assistant-chat.js.map +1 -1
  11. package/dist-client/components/chat-echo-dedup.d.ts +1 -0
  12. package/dist-client/components/chat-echo-dedup.js +1 -0
  13. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  14. package/dist-client/components/reply-message.d.ts +5 -0
  15. package/dist-client/components/reply-message.js +14 -0
  16. package/dist-client/components/reply-message.js.map +1 -0
  17. package/dist-client/components/reply-message.test.d.ts +1 -0
  18. package/dist-client/components/reply-message.test.js +16 -0
  19. package/dist-client/components/reply-message.test.js.map +1 -0
  20. package/dist-client/components/roster-identity.d.ts +11 -0
  21. package/dist-client/components/roster-identity.js +11 -0
  22. package/dist-client/components/roster-identity.js.map +1 -0
  23. package/dist-client/components/roster-identity.test.d.ts +1 -0
  24. package/dist-client/components/roster-identity.test.js +20 -0
  25. package/dist-client/components/roster-identity.test.js.map +1 -0
  26. package/dist-client/tsconfig.tsbuildinfo +1 -1
  27. package/dist-client/utils/assistant-navigation.test.js +20 -0
  28. package/dist-client/utils/assistant-navigation.test.js.map +1 -1
  29. package/dist-server/service/chat-session-participant/read-roster.d.ts +9 -0
  30. package/dist-server/service/chat-session-participant/read-roster.js +15 -0
  31. package/dist-server/service/chat-session-participant/read-roster.js.map +1 -0
  32. package/dist-server/service/chat-session-resolver.d.ts +1 -1
  33. package/dist-server/service/chat-session-resolver.js +15 -11
  34. package/dist-server/service/chat-session-resolver.js.map +1 -1
  35. package/dist-server/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/server/service/chat-session-participant/read-roster.test.ts +18 -0
  38. package/server/service/chat-session-participant/read-roster.ts +14 -0
  39. package/server/service/chat-session-resolver.ts +11 -8
  40. package/test/assistant-request-context.test.ts +3 -1
  41. package/translations/en.json +9 -1
  42. package/translations/ko.json +9 -1
@@ -34,4 +34,24 @@ describe('navigation wiring', () => {
34
34
  expect(dock).toContain("errorCode: 'conversation-screen-changed'");
35
35
  });
36
36
  });
37
+ describe('a carried draft survives opening a new conversation', () => {
38
+ test('automatic review starts after cleanup and is sent once per request', () => {
39
+ const source = readFileSync(join(__dirname, '../components/assistant-chat.ts'), 'utf8');
40
+ const update = source.slice(source.indexOf(' updated(changed:'), source.indexOf(' disconnectedCallback()', source.indexOf(' updated(changed:')));
41
+ const placement = update.indexOf('if (this.autoAsk &&');
42
+ expect(placement).toBeGreaterThan(update.indexOf("if (changed.has('sessionId')"));
43
+ expect(update.slice(placement)).not.toContain("this.input = ''");
44
+ expect(update.slice(placement)).toContain('!== this._autoAsked');
45
+ expect(update.slice(placement)).toContain('void this.send()');
46
+ });
47
+ test('draft placement follows subject, mode and session cleanup', () => {
48
+ const source = readFileSync(join(__dirname, '../components/assistant-chat.ts'), 'utf8');
49
+ const update = source.slice(source.indexOf(' updated(changed:'), source.indexOf(' disconnectedCallback()', source.indexOf(' updated(changed:')));
50
+ const placement = update.indexOf('const draft = assistantDraftDecision(');
51
+ expect(placement).toBeGreaterThan(update.indexOf("if (changed.has('subject'))"));
52
+ expect(placement).toBeGreaterThan(update.indexOf("if (changed.has('mode')"));
53
+ expect(placement).toBeGreaterThan(update.indexOf("if (changed.has('sessionId')"));
54
+ expect(update.slice(placement)).not.toContain("this.input = ''");
55
+ });
56
+ });
37
57
  //# sourceMappingURL=assistant-navigation.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-navigation.test.js","sourceRoot":"","sources":["../../client/utils/assistant-navigation.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,QAAQ,CAAC,oEAAoE,EAAE,GAAG,EAAE;IAClF,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,iDAAiD,EAAE,MAAM,CAAC,EAAE;QACnF,KAAK,MAAM,WAAW,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,CAAC,iBAAiB,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACzF,CAAC;IACH,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,iBAAiB,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACzG,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,iBAAiB,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACvG,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,iBAAiB,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACxG,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,iCAAiC,CAAC,EAAE,MAAM,CAAC,CAAA;IACrF,IAAI,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC5E,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAA;QACxD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yCAAyC,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACtE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAA;QACrD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACtF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAA;QAC9D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QACvE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,0CAA0C,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { surfaceTransition } from './assistant-navigation.js'\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\ndescribe('a screen offers context without taking over an active conversation', () => {\n test.each([false, true])('a different identity resets even when active=%s', active => {\n for (const sameSubject of [false, true]) {\n expect(surfaceTransition({ identityChanged: true, sameSubject, active })).toBe('reset')\n }\n })\n test('an empty conversation follows the screen', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: false, active: false })).toBe('follow')\n })\n test('an existing conversation, draft or ongoing response offers a switch', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: false, active: true })).toBe('offer')\n })\n test('returning to the same subject refreshes without reopening', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: true, active: true })).toBe('refresh')\n })\n})\n\ndescribe('navigation wiring', () => {\n const dock = readFileSync(join(__dirname, '../components/assistant-dock.ts'), 'utf8')\n test('history hides rather than unmounts the chat and its unsent input', () => {\n expect(dock).toContain('?hidden=${this.showingHistory}')\n expect(dock).toContain('ready && !this.loading && !this.failure')\n })\n test('identity changes clear a carried ask and reread capability', () => {\n expect(dock).toContain(\"if (transition === 'reset')\")\n expect(dock).toContain('this.asking = undefined')\n expect(dock).toContain(\"this.capability = 'unknown'\")\n })\n test('all direct board actions check the current screen, not the retained editor', () => {\n expect(dock).toContain('if (!this.mayApplyOnScreen()) return')\n expect(dock.match(/if \\(this.mayApplyOnScreen\\(\\)\\)/g)).toHaveLength(2)\n expect(dock).toContain(\"errorCode: 'conversation-screen-changed'\")\n })\n})\n"]}
1
+ {"version":3,"file":"assistant-navigation.test.js","sourceRoot":"","sources":["../../client/utils/assistant-navigation.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { surfaceTransition } from './assistant-navigation.js'\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\ndescribe('a screen offers context without taking over an active conversation', () => {\n test.each([false, true])('a different identity resets even when active=%s', active => {\n for (const sameSubject of [false, true]) {\n expect(surfaceTransition({ identityChanged: true, sameSubject, active })).toBe('reset')\n }\n })\n test('an empty conversation follows the screen', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: false, active: false })).toBe('follow')\n })\n test('an existing conversation, draft or ongoing response offers a switch', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: false, active: true })).toBe('offer')\n })\n test('returning to the same subject refreshes without reopening', () => {\n expect(surfaceTransition({ identityChanged: false, sameSubject: true, active: true })).toBe('refresh')\n })\n})\n\ndescribe('navigation wiring', () => {\n const dock = readFileSync(join(__dirname, '../components/assistant-dock.ts'), 'utf8')\n test('history hides rather than unmounts the chat and its unsent input', () => {\n expect(dock).toContain('?hidden=${this.showingHistory}')\n expect(dock).toContain('ready && !this.loading && !this.failure')\n })\n test('identity changes clear a carried ask and reread capability', () => {\n expect(dock).toContain(\"if (transition === 'reset')\")\n expect(dock).toContain('this.asking = undefined')\n expect(dock).toContain(\"this.capability = 'unknown'\")\n })\n test('all direct board actions check the current screen, not the retained editor', () => {\n expect(dock).toContain('if (!this.mayApplyOnScreen()) return')\n expect(dock.match(/if \\(this.mayApplyOnScreen\\(\\)\\)/g)).toHaveLength(2)\n expect(dock).toContain(\"errorCode: 'conversation-screen-changed'\")\n })\n})\n\n\ndescribe('a carried draft survives opening a new conversation', () => {\n test('automatic review starts after cleanup and is sent once per request', () => {\n const source = readFileSync(join(__dirname, '../components/assistant-chat.ts'), 'utf8')\n const update = source.slice(source.indexOf(' updated(changed:'), source.indexOf(' disconnectedCallback()', source.indexOf(' updated(changed:')))\n const placement = update.indexOf('if (this.autoAsk &&')\n expect(placement).toBeGreaterThan(update.indexOf(\"if (changed.has('sessionId')\"))\n expect(update.slice(placement)).not.toContain(\"this.input = ''\")\n expect(update.slice(placement)).toContain('!== this._autoAsked')\n expect(update.slice(placement)).toContain('void this.send()')\n })\n test('draft placement follows subject, mode and session cleanup', () => {\n const source = readFileSync(join(__dirname, '../components/assistant-chat.ts'), 'utf8')\n const update = source.slice(source.indexOf(' updated(changed:'), source.indexOf(' disconnectedCallback()', source.indexOf(' updated(changed:')))\n const placement = update.indexOf('const draft = assistantDraftDecision(')\n expect(placement).toBeGreaterThan(update.indexOf(\"if (changed.has('subject'))\"))\n expect(placement).toBeGreaterThan(update.indexOf(\"if (changed.has('mode')\"))\n expect(placement).toBeGreaterThan(update.indexOf(\"if (changed.has('sessionId')\"))\n expect(update.slice(placement)).not.toContain(\"this.input = ''\")\n })\n})\n"]}
@@ -0,0 +1,9 @@
1
+ /** Read the room roster with user identities hydrated, after applying the room's visibility gate. */
2
+ export declare function readRoster<T>(room: {
3
+ id: string;
4
+ domainId: string;
5
+ visibility?: string;
6
+ }, port: {
7
+ isParticipant(): Promise<boolean>;
8
+ find(options: any): Promise<T[]>;
9
+ }): Promise<T[]>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readRoster = readRoster;
4
+ const session_visibility_js_1 = require("../chat-session/session-visibility.js");
5
+ /** Read the room roster with user identities hydrated, after applying the room's visibility gate. */
6
+ async function readRoster(room, port) {
7
+ if (!(0, session_visibility_js_1.mayReadSession)(room.visibility, room.visibility === 'private' ? await port.isParticipant() : false))
8
+ return [];
9
+ return port.find({
10
+ where: { session: { id: room.id }, domain: { id: room.domainId } },
11
+ relations: ['user'],
12
+ order: { createdAt: 'ASC' }
13
+ });
14
+ }
15
+ //# sourceMappingURL=read-roster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-roster.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/read-roster.ts"],"names":[],"mappings":";;AAGA,gCAUC;AAbD,iFAAsE;AAEtE,qGAAqG;AAC9F,KAAK,UAAU,UAAU,CAC9B,IAA2D,EAC3D,IAA6E;IAE7E,IAAI,CAAC,IAAA,sCAAc,EAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnH,OAAO,IAAI,CAAC,IAAI,CAAC;QACf,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE;QAClE,SAAS,EAAE,CAAC,MAAM,CAAC;QACnB,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;KAC5B,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { mayReadSession } from '../chat-session/session-visibility.js'\n\n/** Read the room roster with user identities hydrated, after applying the room's visibility gate. */\nexport async function readRoster<T>(\n room: { id: string; domainId: string; visibility?: string },\n port: { isParticipant(): Promise<boolean>; find(options: any): Promise<T[]> }\n): Promise<T[]> {\n if (!mayReadSession(room.visibility, room.visibility === 'private' ? await port.isParticipant() : false)) return []\n return port.find({\n where: { session: { id: room.id }, domain: { id: room.domainId } },\n relations: ['user'],\n order: { createdAt: 'ASC' }\n })\n}\n"]}
@@ -116,7 +116,7 @@ export declare class ChatSessionResolver {
116
116
  * attachment policy. The size is known only once stored, so a file over the limit is removed and refused.
117
117
  */
118
118
  attachToConversation(sessionId: string, files: FileUpload[], context: ResolverContext, kind?: string): Promise<ChatAttachmentInfo[]>;
119
- sayToRoom(sessionId: string, content: string, context: ResolverContext): Promise<string | null>;
119
+ sayToRoom(sessionId: string, content: string, replyToId: string | undefined, context: ResolverContext): Promise<string | null>;
120
120
  appendSessionNote(sessionId: string, content: string, context: ResolverContext): Promise<boolean>;
121
121
  appendSessionTurns(sessionId: string, turns: {
122
122
  role?: string;
@@ -23,6 +23,7 @@ const reader_gate_js_1 = require("./chat-message/reader-gate.js");
23
23
  const session_visibility_js_1 = require("./chat-session/session-visibility.js");
24
24
  const mention_invite_js_1 = require("./chat-session-participant/mention-invite.js");
25
25
  const invite_notification_js_1 = require("./chat-session-participant/invite-notification.js");
26
+ const read_roster_js_1 = require("./chat-session-participant/read-roster.js");
26
27
  const participant_lookup_js_1 = require("./chat-session-participant/participant-lookup.js");
27
28
  const session_write_gate_js_1 = require("./chat-session/session-write-gate.js");
28
29
  /**
@@ -447,16 +448,16 @@ let ChatSessionResolver = class ChatSessionResolver {
447
448
  return await qb.getMany();
448
449
  }
449
450
  async chatSessionParticipants(sessionId, context) {
450
- const { domain } = context.state;
451
+ const { domain, user } = context.state;
451
452
  const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({
452
453
  id: sessionId,
453
454
  domain: { id: domain.id }
454
455
  });
455
456
  if (!session)
456
457
  return [];
457
- return await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find({
458
- where: { session: { id: sessionId }, domain: { id: domain.id } },
459
- order: { createdAt: 'ASC' }
458
+ return (0, read_roster_js_1.readRoster)({ id: sessionId, domainId: domain.id, visibility: session.visibility }, {
459
+ isParticipant: () => (0, participant_lookup_js_1.isSessionParticipant)(domain.id, sessionId, user?.id),
460
+ find: options => (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find(options)
460
461
  });
461
462
  }
462
463
  async touchPresence(sessionId, context) {
@@ -650,7 +651,7 @@ let ChatSessionResolver = class ChatSessionResolver {
650
651
  }
651
652
  return saved;
652
653
  }
653
- async sayToRoom(sessionId, content, context) {
654
+ async sayToRoom(sessionId, content, replyToId, context) {
654
655
  const { domain, user, tx } = context.state;
655
656
  const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } });
656
657
  if (!session)
@@ -659,10 +660,12 @@ let ChatSessionResolver = class ChatSessionResolver {
659
660
  const body = (0, room_message_js_1.roomMessageBody)(content);
660
661
  if (!body)
661
662
  return null;
662
- const previousLast = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).findOne({
663
- where: { session: { id: session.id } },
664
- order: { createdAt: 'DESC' }
665
- });
663
+ const messages = (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx);
664
+ const previousLast = replyToId
665
+ ? await messages.findOneBy({ id: replyToId, session: { id: session.id } })
666
+ : await messages.findOne({ where: { session: { id: session.id } }, order: { createdAt: 'DESC' } });
667
+ if (replyToId && !previousLast)
668
+ throw new Error('The message to reply to is not in this conversation');
666
669
  const saved = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).save({
667
670
  session: { id: session.id },
668
671
  role: 'user',
@@ -1099,9 +1102,10 @@ tslib_1.__decorate([
1099
1102
  (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
1100
1103
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1101
1104
  tslib_1.__param(1, (0, type_graphql_1.Arg)('content')),
1102
- tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
1105
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('replyToId', { nullable: true })),
1106
+ tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
1103
1107
  tslib_1.__metadata("design:type", Function),
1104
- tslib_1.__metadata("design:paramtypes", [String, String, Object]),
1108
+ tslib_1.__metadata("design:paramtypes", [String, String, String, Object]),
1105
1109
  tslib_1.__metadata("design:returntype", Promise)
1106
1110
  ], ChatSessionResolver.prototype, "sayToRoom", null);
1107
1111
  tslib_1.__decorate([