@things-factory/ai-assistant 10.1.47 → 10.1.50

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 (160) hide show
  1. package/client/components/assistant-chat.ts +97 -19
  2. package/client/components/assistant-dock.ts +617 -0
  3. package/client/components/assistant-mode.ts +2 -0
  4. package/client/components/assistant-session-manager.ts +607 -0
  5. package/client/components/chat-echo-dedup.ts +4 -0
  6. package/client/components/chat-input-builder.ts +4 -1
  7. package/client/components/panel-aria.test.ts +13 -8
  8. package/client/index.ts +8 -0
  9. package/client/setup-assistant-dock.ts +109 -0
  10. package/client/utils/assistant-activity.ts +122 -0
  11. package/client/utils/assistant-ask.ts +44 -0
  12. package/client/utils/assistant-proposals.ts +50 -0
  13. package/client/utils/assistant-session-actions.ts +18 -0
  14. package/client/utils/assistant-session-admin.ts +167 -0
  15. package/client/utils/assistant-session-controller.ts +2 -0
  16. package/client/utils/assistant-session-transport.ts +6 -2
  17. package/client/utils/assistant-surface.ts +157 -0
  18. package/client/utils/attachment-policy.ts +71 -0
  19. package/dist-client/components/assistant-chat.d.ts +6 -0
  20. package/dist-client/components/assistant-chat.js +98 -20
  21. package/dist-client/components/assistant-chat.js.map +1 -1
  22. package/dist-client/components/assistant-dock.d.ts +74 -0
  23. package/dist-client/components/assistant-dock.js +641 -0
  24. package/dist-client/components/assistant-dock.js.map +1 -0
  25. package/dist-client/components/assistant-mode.d.ts +6 -0
  26. package/dist-client/components/assistant-mode.js.map +1 -1
  27. package/dist-client/components/assistant-session-manager.d.ts +77 -0
  28. package/dist-client/components/assistant-session-manager.js +652 -0
  29. package/dist-client/components/assistant-session-manager.js.map +1 -0
  30. package/dist-client/components/chat-echo-dedup.js +4 -0
  31. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  32. package/dist-client/components/chat-input-builder.d.ts +2 -0
  33. package/dist-client/components/chat-input-builder.js +2 -1
  34. package/dist-client/components/chat-input-builder.js.map +1 -1
  35. package/dist-client/components/panel-aria.test.js +12 -8
  36. package/dist-client/components/panel-aria.test.js.map +1 -1
  37. package/dist-client/index.d.ts +8 -0
  38. package/dist-client/index.js +8 -0
  39. package/dist-client/index.js.map +1 -1
  40. package/dist-client/setup-assistant-dock.d.ts +15 -0
  41. package/dist-client/setup-assistant-dock.js +110 -0
  42. package/dist-client/setup-assistant-dock.js.map +1 -0
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/dist-client/utils/assistant-activity.d.ts +22 -0
  45. package/dist-client/utils/assistant-activity.js +104 -0
  46. package/dist-client/utils/assistant-activity.js.map +1 -0
  47. package/dist-client/utils/assistant-ask.d.ts +23 -0
  48. package/dist-client/utils/assistant-ask.js +18 -0
  49. package/dist-client/utils/assistant-ask.js.map +1 -0
  50. package/dist-client/utils/assistant-proposals.d.ts +29 -0
  51. package/dist-client/utils/assistant-proposals.js +22 -0
  52. package/dist-client/utils/assistant-proposals.js.map +1 -0
  53. package/dist-client/utils/assistant-session-actions.d.ts +12 -0
  54. package/dist-client/utils/assistant-session-actions.js +19 -0
  55. package/dist-client/utils/assistant-session-actions.js.map +1 -0
  56. package/dist-client/utils/assistant-session-admin.d.ts +72 -0
  57. package/dist-client/utils/assistant-session-admin.js +81 -0
  58. package/dist-client/utils/assistant-session-admin.js.map +1 -0
  59. package/dist-client/utils/assistant-session-controller.d.ts +2 -0
  60. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  61. package/dist-client/utils/assistant-session-transport.js +6 -2
  62. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  63. package/dist-client/utils/assistant-surface.d.ts +72 -0
  64. package/dist-client/utils/assistant-surface.js +99 -0
  65. package/dist-client/utils/assistant-surface.js.map +1 -0
  66. package/dist-client/utils/attachment-policy.d.ts +35 -0
  67. package/dist-client/utils/attachment-policy.js +56 -0
  68. package/dist-client/utils/attachment-policy.js.map +1 -0
  69. package/dist-server/service/assistant-chat-resolver.d.ts +1 -0
  70. package/dist-server/service/assistant-chat-resolver.js +71 -3
  71. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  72. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +1 -0
  73. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +7 -0
  74. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  75. package/dist-server/service/assistant-mode/resolve.d.ts +3 -0
  76. package/dist-server/service/assistant-mode/resolve.js +6 -2
  77. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  78. package/dist-server/service/assistant-mode/types.d.ts +6 -0
  79. package/dist-server/service/assistant-mode/types.js +0 -9
  80. package/dist-server/service/assistant-mode/types.js.map +1 -1
  81. package/dist-server/service/chat-message/chat-message.d.ts +6 -0
  82. package/dist-server/service/chat-message/chat-message.js +19 -0
  83. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  84. package/dist-server/service/chat-session/attachment-policy.d.ts +44 -0
  85. package/dist-server/service/chat-session/attachment-policy.js +56 -0
  86. package/dist-server/service/chat-session/attachment-policy.js.map +1 -0
  87. package/dist-server/service/chat-session/attachment-route.d.ts +3 -0
  88. package/dist-server/service/chat-session/attachment-route.js +44 -0
  89. package/dist-server/service/chat-session/attachment-route.js.map +1 -0
  90. package/dist-server/service/chat-session/attachment-tools.d.ts +3 -0
  91. package/dist-server/service/chat-session/attachment-tools.js +59 -0
  92. package/dist-server/service/chat-session/attachment-tools.js.map +1 -0
  93. package/dist-server/service/chat-session/session-attachments.d.ts +36 -0
  94. package/dist-server/service/chat-session/session-attachments.js +84 -0
  95. package/dist-server/service/chat-session/session-attachments.js.map +1 -0
  96. package/dist-server/service/chat-session/session-inbox.d.ts +29 -0
  97. package/dist-server/service/chat-session/session-inbox.js +32 -8
  98. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  99. package/dist-server/service/chat-session/session-purge.d.ts +14 -0
  100. package/dist-server/service/chat-session/session-purge.js +77 -0
  101. package/dist-server/service/chat-session/session-purge.js.map +1 -0
  102. package/dist-server/service/chat-session/session-retention.d.ts +6 -0
  103. package/dist-server/service/chat-session/session-retention.js +20 -0
  104. package/dist-server/service/chat-session/session-retention.js.map +1 -0
  105. package/dist-server/service/chat-session/session-visibility.d.ts +8 -0
  106. package/dist-server/service/chat-session/session-visibility.js +11 -0
  107. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  108. package/dist-server/service/chat-session/session-write-gate.d.ts +11 -0
  109. package/dist-server/service/chat-session/session-write-gate.js +24 -0
  110. package/dist-server/service/chat-session/session-write-gate.js.map +1 -0
  111. package/dist-server/service/chat-session-participant/invite-notification.d.ts +1 -1
  112. package/dist-server/service/chat-session-participant/invite-notification.js +21 -6
  113. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
  114. package/dist-server/service/chat-session-participant/participant-lookup-roles.d.ts +3 -0
  115. package/dist-server/service/chat-session-participant/participant-lookup-roles.js +10 -0
  116. package/dist-server/service/chat-session-participant/participant-lookup-roles.js.map +1 -0
  117. package/dist-server/service/chat-session-participant/participant-lookup.d.ts +2 -0
  118. package/dist-server/service/chat-session-participant/participant-lookup.js +26 -0
  119. package/dist-server/service/chat-session-participant/participant-lookup.js.map +1 -0
  120. package/dist-server/service/chat-session-resolver.d.ts +17 -2
  121. package/dist-server/service/chat-session-resolver.js +135 -15
  122. package/dist-server/service/chat-session-resolver.js.map +1 -1
  123. package/dist-server/service/index.d.ts +5 -0
  124. package/dist-server/service/index.js +5 -0
  125. package/dist-server/service/index.js.map +1 -1
  126. package/dist-server/tsconfig.tsbuildinfo +1 -1
  127. package/package.json +9 -2
  128. package/server/service/assistant-chat-resolver.ts +71 -4
  129. package/server/service/assistant-mode/assistant-mode-resolver.ts +6 -0
  130. package/server/service/assistant-mode/resolve.ts +9 -2
  131. package/server/service/assistant-mode/types.ts +6 -0
  132. package/server/service/chat-message/chat-message.ts +17 -0
  133. package/server/service/chat-session/attachment-policy.test.ts +42 -0
  134. package/server/service/chat-session/attachment-policy.ts +68 -0
  135. package/server/service/chat-session/attachment-route.ts +45 -0
  136. package/server/service/chat-session/attachment-tools.ts +63 -0
  137. package/server/service/chat-session/session-attachments.ts +89 -0
  138. package/server/service/chat-session/session-inbox.test.ts +71 -1
  139. package/server/service/chat-session/session-inbox.ts +61 -11
  140. package/server/service/chat-session/session-purge.test.ts +24 -0
  141. package/server/service/chat-session/session-purge.ts +81 -0
  142. package/server/service/chat-session/session-retention.ts +17 -0
  143. package/server/service/chat-session/session-visibility.ts +11 -0
  144. package/server/service/chat-session/session-write-gate.test.ts +53 -0
  145. package/server/service/chat-session/session-write-gate.ts +31 -0
  146. package/server/service/chat-session-participant/invite-notification.test.ts +15 -0
  147. package/server/service/chat-session-participant/invite-notification.ts +24 -6
  148. package/server/service/chat-session-participant/participant-lookup-roles.ts +6 -0
  149. package/server/service/chat-session-participant/participant-lookup.ts +22 -0
  150. package/server/service/chat-session-resolver.ts +130 -13
  151. package/server/service/index.ts +5 -0
  152. package/test/assistant-dock.test.ts +118 -0
  153. package/test/assistant-session-manager.test.ts +126 -0
  154. package/test/attachment-policy-agree.test.ts +32 -0
  155. package/test/open-doors.test.ts +5 -2
  156. package/translations/en.json +61 -1
  157. package/translations/ja.json +61 -1
  158. package/translations/ko.json +61 -1
  159. package/translations/ms.json +61 -1
  160. package/translations/zh.json +61 -1
@@ -0,0 +1,22 @@
1
+ export interface SessionActivity {
2
+ sessionId: string;
3
+ kind: 'message' | 'meta';
4
+ anchorType?: string;
5
+ anchorId?: string;
6
+ at: string;
7
+ preview?: string;
8
+ actorId?: string;
9
+ }
10
+ type Listener = (activity: SessionActivity) => void;
11
+ export declare function unreadSessionCount(): number;
12
+ export declare function isSessionUnread(sessionId: string): boolean;
13
+ /** 그 세션을 읽었다 — 도크가 세션을 열 때 호출(서버 markSessionRead 와 짝). */
14
+ export declare function clearSessionUnread(sessionId: string): void;
15
+ export declare function subscribeSessionActivity(fn: Listener): () => void;
16
+ /**
17
+ * 구독 시작 — 부팅에서 1회. 재호출은 무시(중복 구독 방지).
18
+ * 실패해도 앱을 막지 않는다 — 배지가 없을 뿐이고, 도크를 열면 목록을 직접 읽는다.
19
+ */
20
+ export declare function startSessionActivityFeed(): void;
21
+ export declare function setOpenSession(sessionId: string): void;
22
+ export {};
@@ -0,0 +1,104 @@
1
+ /*
2
+ * AI 세션 활동 구독 — **앱 전역 1회**. 도크가 닫혀 있어도(마운트되지 않아도) 새 소식을 안다.
3
+ *
4
+ * operato-twin 의 `twin-ai-activity.ts` 에서 올려 왔다(2026-09-25, design/ai/shared-dock.md §2-8) — 안 읽음은
5
+ * 모든 앱에서 같은 기능이다. `setupAssistantDock()` 이 부팅에서 한 번 연다.
6
+ *
7
+ * ── 왜 도크가 아니라 여기인가 ───────────────────────────────────────────────
8
+ * 도크는 asidebar viewpart 이고 닫혀 있으면 **렌더되지 않는다**(템플릿이 아예 안 붙는다). 그래서
9
+ * 도크 안에서 구독하면 닫아 둔 동안 아무 소식을 받지 못한다 — 배지를 띄울 수 없다.
10
+ * 구독은 부팅 시 한 번 열고, 도크·컨텍스트 밴드는 이 스토어를 본다.
11
+ *
12
+ * ── 종류에 따라 다르게 다룬다 ───────────────────────────────────────────────
13
+ * message — 새 메시지. 안 읽은 세션으로 표시(배지 올림). **내가 보낸 것은 올리지 않는다.**
14
+ * meta — 이름 변경 등. 목록 갱신만 알리고 배지는 올리지 않는다(소음 방지).
15
+ *
16
+ * 정확한 미읽음 **개수**는 세지 않는다 — 세션 수만 센다(서버 목록 정책과 같다).
17
+ */
18
+ import gql from 'graphql-tag';
19
+ import { client, gqlContext } from '@operato/graphql';
20
+ const listeners = new Set();
21
+ /** 안 읽은 세션 id — 개수가 아니라 세션 단위. 도크에서 그 세션을 열면 지운다. */
22
+ const unreadSessions = new Set();
23
+ let subscription = null;
24
+ const ACTIVITY_SUBSCRIPTION = gql `
25
+ subscription MySessionActivity {
26
+ mySessionActivity {
27
+ sessionId
28
+ kind
29
+ anchorType
30
+ anchorId
31
+ at
32
+ preview
33
+ actorId
34
+ }
35
+ }
36
+ `;
37
+ export function unreadSessionCount() {
38
+ return unreadSessions.size;
39
+ }
40
+ export function isSessionUnread(sessionId) {
41
+ return unreadSessions.has(sessionId);
42
+ }
43
+ /** 그 세션을 읽었다 — 도크가 세션을 열 때 호출(서버 markSessionRead 와 짝). */
44
+ export function clearSessionUnread(sessionId) {
45
+ if (unreadSessions.delete(sessionId))
46
+ notify({ sessionId, kind: 'meta', at: new Date().toISOString() });
47
+ }
48
+ export function subscribeSessionActivity(fn) {
49
+ listeners.add(fn);
50
+ return () => listeners.delete(fn);
51
+ }
52
+ function notify(activity) {
53
+ listeners.forEach(fn => {
54
+ try {
55
+ fn(activity);
56
+ }
57
+ catch {
58
+ /* 구독자 오류가 다른 구독자를 막지 않는다 */
59
+ }
60
+ });
61
+ }
62
+ /**
63
+ * 구독 시작 — 부팅에서 1회. 재호출은 무시(중복 구독 방지).
64
+ * 실패해도 앱을 막지 않는다 — 배지가 없을 뿐이고, 도크를 열면 목록을 직접 읽는다.
65
+ */
66
+ export function startSessionActivityFeed() {
67
+ if (subscription)
68
+ return;
69
+ try {
70
+ subscription = client
71
+ .subscribe({ query: ACTIVITY_SUBSCRIPTION, context: gqlContext() })
72
+ .subscribe({
73
+ next: ({ data }) => {
74
+ const activity = data?.mySessionActivity;
75
+ if (!activity?.sessionId)
76
+ return;
77
+ /* 자기 발신은 배지를 올리지 않는다 — 방금 내가 쓴 것이 안 읽음으로 뜨면 어색하다.
78
+ * 서버는 actorId(사용자 id)를 싣지만 클라는 내부 관리키를 판정에 쓰지 않는다 →
79
+ * 자기 발신 판정은 도크가 열려 있는 세션인지로 대신한다(그 세션은 이미 읽고 있다). */
80
+ if (activity.kind === 'message' && !isOpenSession(activity.sessionId)) {
81
+ unreadSessions.add(activity.sessionId);
82
+ }
83
+ notify(activity);
84
+ },
85
+ error: () => {
86
+ subscription = null; // 재연결은 다음 호출에 맡긴다(자동 재시도 루프를 두지 않는다)
87
+ }
88
+ });
89
+ }
90
+ catch {
91
+ subscription = null;
92
+ }
93
+ }
94
+ /* 도크가 지금 열어 둔 세션 — 그 세션의 활동은 배지를 올리지 않는다(보고 있으므로). */
95
+ let openSessionId = '';
96
+ export function setOpenSession(sessionId) {
97
+ openSessionId = sessionId || '';
98
+ if (openSessionId)
99
+ clearSessionUnread(openSessionId);
100
+ }
101
+ function isOpenSession(sessionId) {
102
+ return !!openSessionId && openSessionId === sessionId;
103
+ }
104
+ //# sourceMappingURL=assistant-activity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-activity.js","sourceRoot":"","sources":["../../client/utils/assistant-activity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,GAAG,MAAM,aAAa,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAerD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAA;AACrC,qDAAqD;AACrD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAA;AACxC,IAAI,YAAY,GAAmC,IAAI,CAAA;AAEvD,MAAM,qBAAqB,GAAG,GAAG,CAAA;;;;;;;;;;;;CAYhC,CAAA;AAGD,MAAM,UAAU,kBAAkB;IAChC,OAAO,cAAc,CAAC,IAAI,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,OAAO,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,IAAI,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC;QAAE,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AACzG,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,EAAY;IACnD,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACjB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AACnC,CAAC;AAED,SAAS,MAAM,CAAC,QAAyB;IACvC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;QACrB,IAAI,CAAC;YACH,EAAE,CAAC,QAAQ,CAAC,CAAA;QACd,CAAC;QAAC,MAAM,CAAC;YACP,4BAA4B;QAC9B,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,IAAI,YAAY;QAAE,OAAM;IACxB,IAAI,CAAC;QACH,YAAY,GAAG,MAAM;aAClB,SAAS,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,CAAC;aAClE,SAAS,CAAC;YACT,IAAI,EAAE,CAAC,EAAE,IAAI,EAAO,EAAE,EAAE;gBACtB,MAAM,QAAQ,GAAgC,IAAI,EAAE,iBAAiB,CAAA;gBACrE,IAAI,CAAC,QAAQ,EAAE,SAAS;oBAAE,OAAM;gBAChC;;qEAEqD;gBACrD,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;oBACtE,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;gBACxC,CAAC;gBACD,MAAM,CAAC,QAAQ,CAAC,CAAA;YAClB,CAAC;YACD,KAAK,EAAE,GAAG,EAAE;gBACV,YAAY,GAAG,IAAI,CAAA,CAAC,qCAAqC;YAC3D,CAAC;SACF,CAAC,CAAA;IACN,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,IAAI,aAAa,GAAG,EAAE,CAAA;AACtB,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,aAAa,GAAG,SAAS,IAAI,EAAE,CAAA;IAC/B,IAAI,aAAa;QAAE,kBAAkB,CAAC,aAAa,CAAC,CAAA;AACtD,CAAC;AACD,SAAS,aAAa,CAAC,SAAiB;IACtC,OAAO,CAAC,CAAC,aAAa,IAAI,aAAa,KAAK,SAAS,CAAA;AACvD,CAAC","sourcesContent":["/*\n * AI 세션 활동 구독 — **앱 전역 1회**. 도크가 닫혀 있어도(마운트되지 않아도) 새 소식을 안다.\n *\n * operato-twin 의 `twin-ai-activity.ts` 에서 올려 왔다(2026-09-25, design/ai/shared-dock.md §2-8) — 안 읽음은\n * 모든 앱에서 같은 기능이다. `setupAssistantDock()` 이 부팅에서 한 번 연다.\n *\n * ── 왜 도크가 아니라 여기인가 ───────────────────────────────────────────────\n * 도크는 asidebar viewpart 이고 닫혀 있으면 **렌더되지 않는다**(템플릿이 아예 안 붙는다). 그래서\n * 도크 안에서 구독하면 닫아 둔 동안 아무 소식을 받지 못한다 — 배지를 띄울 수 없다.\n * 구독은 부팅 시 한 번 열고, 도크·컨텍스트 밴드는 이 스토어를 본다.\n *\n * ── 종류에 따라 다르게 다룬다 ───────────────────────────────────────────────\n * message — 새 메시지. 안 읽은 세션으로 표시(배지 올림). **내가 보낸 것은 올리지 않는다.**\n * meta — 이름 변경 등. 목록 갱신만 알리고 배지는 올리지 않는다(소음 방지).\n *\n * 정확한 미읽음 **개수**는 세지 않는다 — 세션 수만 센다(서버 목록 정책과 같다).\n */\nimport gql from 'graphql-tag'\n\nimport { client, gqlContext } from '@operato/graphql'\n\n\nexport interface SessionActivity {\n sessionId: string\n kind: 'message' | 'meta'\n anchorType?: string\n anchorId?: string\n at: string\n preview?: string\n actorId?: string\n}\n\ntype Listener = (activity: SessionActivity) => void\n\nconst listeners = new Set<Listener>()\n/** 안 읽은 세션 id — 개수가 아니라 세션 단위. 도크에서 그 세션을 열면 지운다. */\nconst unreadSessions = new Set<string>()\nlet subscription: { unsubscribe(): void } | null = null\n\nconst ACTIVITY_SUBSCRIPTION = gql`\n subscription MySessionActivity {\n mySessionActivity {\n sessionId\n kind\n anchorType\n anchorId\n at\n preview\n actorId\n }\n }\n`\n\n\nexport function unreadSessionCount(): number {\n return unreadSessions.size\n}\n\nexport function isSessionUnread(sessionId: string): boolean {\n return unreadSessions.has(sessionId)\n}\n\n/** 그 세션을 읽었다 — 도크가 세션을 열 때 호출(서버 markSessionRead 와 짝). */\nexport function clearSessionUnread(sessionId: string): void {\n if (unreadSessions.delete(sessionId)) notify({ sessionId, kind: 'meta', at: new Date().toISOString() })\n}\n\nexport function subscribeSessionActivity(fn: Listener): () => void {\n listeners.add(fn)\n return () => listeners.delete(fn)\n}\n\nfunction notify(activity: SessionActivity) {\n listeners.forEach(fn => {\n try {\n fn(activity)\n } catch {\n /* 구독자 오류가 다른 구독자를 막지 않는다 */\n }\n })\n}\n\n/**\n * 구독 시작 — 부팅에서 1회. 재호출은 무시(중복 구독 방지).\n * 실패해도 앱을 막지 않는다 — 배지가 없을 뿐이고, 도크를 열면 목록을 직접 읽는다.\n */\nexport function startSessionActivityFeed(): void {\n if (subscription) return\n try {\n subscription = client\n .subscribe({ query: ACTIVITY_SUBSCRIPTION, context: gqlContext() })\n .subscribe({\n next: ({ data }: any) => {\n const activity: SessionActivity | undefined = data?.mySessionActivity\n if (!activity?.sessionId) return\n /* 자기 발신은 배지를 올리지 않는다 — 방금 내가 쓴 것이 안 읽음으로 뜨면 어색하다.\n * 서버는 actorId(사용자 id)를 싣지만 클라는 내부 관리키를 판정에 쓰지 않는다 →\n * 자기 발신 판정은 도크가 열려 있는 세션인지로 대신한다(그 세션은 이미 읽고 있다). */\n if (activity.kind === 'message' && !isOpenSession(activity.sessionId)) {\n unreadSessions.add(activity.sessionId)\n }\n notify(activity)\n },\n error: () => {\n subscription = null // 재연결은 다음 호출에 맡긴다(자동 재시도 루프를 두지 않는다)\n }\n })\n } catch {\n subscription = null\n }\n}\n\n/* 도크가 지금 열어 둔 세션 — 그 세션의 활동은 배지를 올리지 않는다(보고 있으므로). */\nlet openSessionId = ''\nexport function setOpenSession(sessionId: string): void {\n openSessionId = sessionId || ''\n if (openSessionId) clearSessionUnread(openSessionId)\n}\nfunction isOpenSession(sessionId: string): boolean {\n return !!openSessionId && openSessionId === sessionId\n}\n\n"]}
@@ -0,0 +1,23 @@
1
+ export interface AssistantAsk {
2
+ /** Sent as the next question. */
3
+ ask?: string;
4
+ /** Put in the input, not sent. */
5
+ draft?: string;
6
+ /** Ask in the open conversation instead of a new one. */
7
+ continueCurrent?: boolean;
8
+ /** A new conversation's name. */
9
+ name?: string;
10
+ /** Turns carried into the new conversation before the question. */
11
+ transcript?: {
12
+ role: 'user' | 'ai';
13
+ content: string;
14
+ }[];
15
+ /** A note stored first: what the screen was showing when the person asked. */
16
+ context?: string;
17
+ }
18
+ type Listener = () => void;
19
+ export declare function askAssistant(ask: AssistantAsk): void;
20
+ /** The dock takes the request once. */
21
+ export declare function takeAssistantAsk(): AssistantAsk | undefined;
22
+ export declare function subscribeAssistantAsk(listener: Listener): () => void;
23
+ export {};
@@ -0,0 +1,18 @@
1
+ let pending;
2
+ const listeners = new Set();
3
+ export function askAssistant(ask) {
4
+ pending = ask;
5
+ listeners.forEach(listener => listener());
6
+ window.dispatchEvent(new CustomEvent('assistant-dock-open'));
7
+ }
8
+ /** The dock takes the request once. */
9
+ export function takeAssistantAsk() {
10
+ const ask = pending;
11
+ pending = undefined;
12
+ return ask;
13
+ }
14
+ export function subscribeAssistantAsk(listener) {
15
+ listeners.add(listener);
16
+ return () => listeners.delete(listener);
17
+ }
18
+ //# sourceMappingURL=assistant-ask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-ask.js","sourceRoot":"","sources":["../../client/utils/assistant-ask.ts"],"names":[],"mappings":"AAwBA,IAAI,OAAiC,CAAA;AACrC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAA;AAErC,MAAM,UAAU,YAAY,CAAC,GAAiB;IAC5C,OAAO,GAAG,GAAG,CAAA;IACb,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAA;IACzC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,qBAAqB,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAA;IACnB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAkB;IACtD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACzC,CAAC","sourcesContent":["/*\n * Asking the assistant from another screen - `askAssistant()` (design/ai/shared-dock.md §2-11).\n *\n * Generalised from operato-twin's promotion channel and operato-plant's ask box: a toast's \"ask AI\", a\n * header ask box, a performance page carrying its context. The request waits here until the dock takes it,\n * so a dock that is closed (not mounted) still gets it when it opens.\n */\nexport interface AssistantAsk {\n /** Sent as the next question. */\n ask?: string\n /** Put in the input, not sent. */\n draft?: string\n /** Ask in the open conversation instead of a new one. */\n continueCurrent?: boolean\n /** A new conversation's name. */\n name?: string\n /** Turns carried into the new conversation before the question. */\n transcript?: { role: 'user' | 'ai'; content: string }[]\n /** A note stored first: what the screen was showing when the person asked. */\n context?: string\n}\n\ntype Listener = () => void\n\nlet pending: AssistantAsk | undefined\nconst listeners = new Set<Listener>()\n\nexport function askAssistant(ask: AssistantAsk): void {\n pending = ask\n listeners.forEach(listener => listener())\n window.dispatchEvent(new CustomEvent('assistant-dock-open'))\n}\n\n/** The dock takes the request once. */\nexport function takeAssistantAsk(): AssistantAsk | undefined {\n const ask = pending\n pending = undefined\n return ask\n}\n\nexport function subscribeAssistantAsk(listener: Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import type { AssistantSurface } from './assistant-surface.js';
2
+ export type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined';
3
+ export interface ProposalOutcome {
4
+ verdict: ProposalVerdict;
5
+ errorCode?: string;
6
+ applied?: number;
7
+ missed?: number;
8
+ /** A line for the conversation, besides the outcome itself (e.g. what was created). */
9
+ note?: string;
10
+ }
11
+ export interface ProposalContext {
12
+ sessionId?: string;
13
+ surface?: AssistantSurface;
14
+ /** Asks the person before anything irreversible. `false` → the handler returns `declined`. */
15
+ confirm(options: {
16
+ title: string;
17
+ text: string;
18
+ confirmText?: string;
19
+ cancelText?: string;
20
+ type?: 'warning' | 'info';
21
+ }): Promise<boolean>;
22
+ notify(level: 'info' | 'error', message: string): void;
23
+ }
24
+ export type ProposalHandler = (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>;
25
+ /** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */
26
+ export declare function registerProposalHandler(tool: string, handler: ProposalHandler): void;
27
+ export declare function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined;
28
+ /** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */
29
+ export declare function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome>;
@@ -0,0 +1,22 @@
1
+ const handlers = new Map();
2
+ /** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */
3
+ export function registerProposalHandler(tool, handler) {
4
+ handlers.set(tool, handler);
5
+ }
6
+ export function proposalHandlerOf(tool) {
7
+ return tool ? handlers.get(tool) : undefined;
8
+ }
9
+ /** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */
10
+ export async function runProposal(proposal, context) {
11
+ const handler = proposalHandlerOf(proposal?.tool);
12
+ if (!handler)
13
+ return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' };
14
+ try {
15
+ const outcome = await handler(proposal, context);
16
+ return outcome?.verdict ? outcome : { verdict: 'unavailable', errorCode: 'no-outcome' };
17
+ }
18
+ catch (error) {
19
+ return { verdict: 'refused', errorCode: error?.message || 'failed' };
20
+ }
21
+ }
22
+ //# sourceMappingURL=assistant-proposals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-proposals.js","sourceRoot":"","sources":["../../client/utils/assistant-proposals.ts"],"names":[],"mappings":"AA4BA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAA;AAEnD,mHAAmH;AACnH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,OAAwB;IAC5E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9C,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAa,EAAE,OAAwB;IACvE,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACjD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAA;IACnF,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChD,OAAO,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IACzF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAG,KAAe,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAA;IACjF,CAAC;AACH,CAAC","sourcesContent":["/*\n * Proposal handlers - each proposal kind is applied by the package that declared its tool\n * (design/ai/shared-dock.md §4). The dock has no kind-specific code: it finds the handler, runs it,\n * and reports the outcome on every branch (ADR-0070). A kind nobody handles is `unavailable`.\n */\nimport type { AssistantSurface } from './assistant-surface.js'\n\nexport type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined'\n\nexport interface ProposalOutcome {\n verdict: ProposalVerdict\n errorCode?: string\n applied?: number\n missed?: number\n /** A line for the conversation, besides the outcome itself (e.g. what was created). */\n note?: string\n}\n\nexport interface ProposalContext {\n sessionId?: string\n surface?: AssistantSurface\n /** Asks the person before anything irreversible. `false` → the handler returns `declined`. */\n confirm(options: { title: string; text: string; confirmText?: string; cancelText?: string; type?: 'warning' | 'info' }): Promise<boolean>\n notify(level: 'info' | 'error', message: string): void\n}\n\nexport type ProposalHandler = (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>\n\nconst handlers = new Map<string, ProposalHandler>()\n\n/** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */\nexport function registerProposalHandler(tool: string, handler: ProposalHandler): void {\n handlers.set(tool, handler)\n}\n\nexport function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined {\n return tool ? handlers.get(tool) : undefined\n}\n\n/** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */\nexport async function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome> {\n const handler = proposalHandlerOf(proposal?.tool)\n if (!handler) return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' }\n try {\n const outcome = await handler(proposal, context)\n return outcome?.verdict ? outcome : { verdict: 'unavailable', errorCode: 'no-outcome' }\n } catch (error) {\n return { verdict: 'refused', errorCode: (error as Error)?.message || 'failed' }\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ export declare function sessionActions(entry: {
2
+ participant?: boolean;
3
+ hidden?: boolean;
4
+ mine?: boolean;
5
+ owner?: boolean;
6
+ }): {
7
+ rename: boolean;
8
+ hide: boolean;
9
+ unhide: boolean;
10
+ remove: boolean;
11
+ setVisibility: boolean;
12
+ };
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Which actions a row of the conversation list offers this viewer - pure, so the rule is tested alone.
3
+ *
4
+ * The same hints the server gives on each inbox entry; the doors decide again:
5
+ * rename any participant (the name is the topic label; reversible)
6
+ * hide/unhide anyone, for their own list only (reversible)
7
+ * delete the creator (`mine`) (not reversible from a screen)
8
+ * visibility the owner (`owner`) (opening shows what is already written)
9
+ */
10
+ export function sessionActions(entry) {
11
+ return {
12
+ rename: entry.participant !== false,
13
+ hide: !entry.hidden,
14
+ unhide: !!entry.hidden,
15
+ remove: !!entry.mine,
16
+ setVisibility: !!entry.owner
17
+ };
18
+ }
19
+ //# sourceMappingURL=assistant-session-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-session-actions.js","sourceRoot":"","sources":["../../client/utils/assistant-session-actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmF;IAChH,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,WAAW,KAAK,KAAK;QACnC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM;QACnB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM;QACtB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI;QACpB,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK;KAC7B,CAAA;AACH,CAAC","sourcesContent":["/*\n * Which actions a row of the conversation list offers this viewer - pure, so the rule is tested alone.\n *\n * The same hints the server gives on each inbox entry; the doors decide again:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n */\nexport function sessionActions(entry: { participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }) {\n return {\n rename: entry.participant !== false,\n hide: !entry.hidden,\n unhide: !!entry.hidden,\n remove: !!entry.mine,\n setVisibility: !!entry.owner\n }\n}\n"]}
@@ -0,0 +1,72 @@
1
+ export { sessionActions } from './assistant-session-actions.js';
2
+ /**
3
+ * Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.
4
+ *
5
+ * These were written once, inside operato-twin's dock (things-factory `b3d1081f4c`, `b44c986c07`,
6
+ * 2026-07-30), and every other dock (plant, figure, warehouse, ecs) had only the tabs. They are the
7
+ * framework's doors (`chatSessionInbox` · `renameAISession` · `hideAISession` · `deleteAISession` ·
8
+ * `setSessionVisibility`), so the screen for them belongs here too - one list every dock opens.
9
+ *
10
+ * Who may do what is the server's to decide; the entry carries hints only:
11
+ * rename any participant (the name is the topic label; reversible)
12
+ * hide/unhide anyone, for their own list only (reversible)
13
+ * delete the creator (`mine`) (not reversible from a screen)
14
+ * visibility the owner (`owner`) (opening shows what is already written)
15
+ */
16
+ export type SessionVisibility = 'open' | 'private';
17
+ /** One row of `chatSessionInbox`, as the list draws it. */
18
+ export interface AssistantSessionEntry {
19
+ id: string;
20
+ name?: string;
21
+ kind?: string;
22
+ lastMessageAt?: string;
23
+ lastMessagePreview?: string;
24
+ previewRedacted?: boolean;
25
+ unread?: boolean;
26
+ participant?: boolean;
27
+ /** I created it - only then may I delete it. */
28
+ mine?: boolean;
29
+ /** I own it - only then may I change who reads it. */
30
+ owner?: boolean;
31
+ /** Hidden from my own list (returned only with the hidden filter on). */
32
+ hidden?: boolean;
33
+ visibility?: SessionVisibility | string;
34
+ }
35
+ /** Legacy single filter - kept for callers written before the filter set. */
36
+ export type AssistantSessionFilter = 'all' | 'mine' | 'hidden';
37
+ /** What the manager narrows by (design/ai/shared-dock.md §2-6). Defaults: this kind, everyone's, any visibility, hidden out. */
38
+ export interface AssistantSessionFilters {
39
+ /** A kind (mode) to list; empty lists every kind on the anchor. */
40
+ kind?: string;
41
+ whose?: 'all' | 'mine' | 'participant';
42
+ visibility?: 'all' | 'open' | 'private';
43
+ includeHidden?: boolean;
44
+ unreadOnly?: boolean;
45
+ }
46
+ export interface AssistantSessionQuery {
47
+ anchorType: string;
48
+ anchorId: string;
49
+ search?: string;
50
+ filter?: AssistantSessionFilter;
51
+ filters?: AssistantSessionFilters;
52
+ limit?: number;
53
+ offset?: number;
54
+ }
55
+ export interface AssistantSessionPage {
56
+ entries: AssistantSessionEntry[];
57
+ /** How many pass the filter - so "load more" knows whether the end of the list is the end of the data. */
58
+ total: number;
59
+ /** How many I hid - so a hidden conversation is never forgotten. */
60
+ hiddenCount: number;
61
+ }
62
+ export interface AssistantSessionAdmin {
63
+ inbox(query: AssistantSessionQuery): Promise<AssistantSessionPage>;
64
+ rename(sessionId: string, name: string): Promise<boolean>;
65
+ hide(sessionId: string, hidden: boolean): Promise<boolean>;
66
+ remove(sessionId: string): Promise<boolean>;
67
+ setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>;
68
+ }
69
+ /** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
70
+ export declare const SESSION_PAGE_MAX = 200;
71
+ /** The framework's session doors, as they are. */
72
+ export declare const assistantSessionAdmin: AssistantSessionAdmin;
@@ -0,0 +1,81 @@
1
+ import { client } from '@operato/graphql';
2
+ import { i18next } from '@operato/i18n';
3
+ import gql from 'graphql-tag';
4
+ export { sessionActions } from './assistant-session-actions.js';
5
+ /** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
6
+ export const SESSION_PAGE_MAX = 200;
7
+ async function mutate(mutation, variables, field) {
8
+ const result = await client.mutate({ mutation, variables });
9
+ if (result.errors?.length)
10
+ throw new Error(result.errors[0].message);
11
+ return result.data?.[field] === true;
12
+ }
13
+ /** The framework's session doors, as they are. */
14
+ export const assistantSessionAdmin = {
15
+ async inbox({ anchorType, anchorId, search, filter = 'all', filters = {}, limit = 50, offset = 0 }) {
16
+ const hidden = filters.includeHidden ?? filter === 'hidden';
17
+ const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine';
18
+ const result = await client.query({
19
+ query: gql `
20
+ query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean) {
21
+ hiddenSessionCount(anchorType: $t, anchorId: $i)
22
+ chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u)
23
+ chatSessionInbox(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, limit: $l, offset: $o) {
24
+ id
25
+ name
26
+ kind
27
+ lastMessageAt
28
+ lastMessagePreview
29
+ previewRedacted
30
+ unread
31
+ participant
32
+ mine
33
+ owner
34
+ hidden
35
+ visibility
36
+ }
37
+ }
38
+ `,
39
+ variables: {
40
+ t: anchorType,
41
+ i: anchorId,
42
+ /* Hidden ones only when asked - elsewhere they stay out of the list. */
43
+ h: hidden,
44
+ q: search?.trim() || null,
45
+ m: mine,
46
+ k: filters.kind || null,
47
+ p: filters.whose === 'participant' || null,
48
+ v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,
49
+ u: filters.unreadOnly || null,
50
+ l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),
51
+ o: Math.max(0, offset)
52
+ },
53
+ fetchPolicy: 'no-cache'
54
+ });
55
+ if (result.errors?.length)
56
+ throw new Error(result.errors[0].message);
57
+ const entries = result.data?.chatSessionInbox;
58
+ if (!Array.isArray(entries)) {
59
+ throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }));
60
+ }
61
+ return {
62
+ /* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */
63
+ entries,
64
+ total: result.data?.chatSessionInboxTotal ?? entries.length,
65
+ hiddenCount: result.data?.hiddenSessionCount ?? 0
66
+ };
67
+ },
68
+ rename(sessionId, name) {
69
+ return mutate(gql `mutation ($id: String!, $n: String!) { renameAISession(sessionId: $id, name: $n) }`, { id: sessionId, n: name }, 'renameAISession');
70
+ },
71
+ hide(sessionId, hidden) {
72
+ return mutate(gql `mutation ($id: String!, $h: Boolean) { hideAISession(sessionId: $id, hidden: $h) }`, { id: sessionId, h: hidden }, 'hideAISession');
73
+ },
74
+ remove(sessionId) {
75
+ return mutate(gql `mutation ($id: String!) { deleteAISession(sessionId: $id) }`, { id: sessionId }, 'deleteAISession');
76
+ },
77
+ setVisibility(sessionId, visibility) {
78
+ return mutate(gql `mutation ($id: String!, $v: String!) { setSessionVisibility(sessionId: $id, visibility: $v) }`, { id: sessionId, v: visibility }, 'setSessionVisibility');
79
+ }
80
+ };
81
+ //# sourceMappingURL=assistant-session-admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-session-admin.js","sourceRoot":"","sources":["../../client/utils/assistant-session-admin.ts"],"names":[],"mappings":"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","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\n\nexport { sessionActions } from './assistant-session-actions.js'\n\n/**\n * Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.\n *\n * These were written once, inside operato-twin's dock (things-factory `b3d1081f4c`, `b44c986c07`,\n * 2026-07-30), and every other dock (plant, figure, warehouse, ecs) had only the tabs. They are the\n * framework's doors (`chatSessionInbox` · `renameAISession` · `hideAISession` · `deleteAISession` ·\n * `setSessionVisibility`), so the screen for them belongs here too - one list every dock opens.\n *\n * Who may do what is the server's to decide; the entry carries hints only:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n */\n\nexport type SessionVisibility = 'open' | 'private'\n\n/** One row of `chatSessionInbox`, as the list draws it. */\nexport interface AssistantSessionEntry {\n id: string\n name?: string\n kind?: string\n lastMessageAt?: string\n lastMessagePreview?: string\n previewRedacted?: boolean\n unread?: boolean\n participant?: boolean\n /** I created it - only then may I delete it. */\n mine?: boolean\n /** I own it - only then may I change who reads it. */\n owner?: boolean\n /** Hidden from my own list (returned only with the hidden filter on). */\n hidden?: boolean\n visibility?: SessionVisibility | string\n}\n\n/** Legacy single filter - kept for callers written before the filter set. */\nexport type AssistantSessionFilter = 'all' | 'mine' | 'hidden'\n\n/** What the manager narrows by (design/ai/shared-dock.md §2-6). Defaults: this kind, everyone's, any visibility, hidden out. */\nexport interface AssistantSessionFilters {\n /** A kind (mode) to list; empty lists every kind on the anchor. */\n kind?: string\n whose?: 'all' | 'mine' | 'participant'\n visibility?: 'all' | 'open' | 'private'\n includeHidden?: boolean\n unreadOnly?: boolean\n}\n\nexport interface AssistantSessionQuery {\n anchorType: string\n anchorId: string\n search?: string\n filter?: AssistantSessionFilter\n filters?: AssistantSessionFilters\n limit?: number\n offset?: number\n}\n\nexport interface AssistantSessionPage {\n entries: AssistantSessionEntry[]\n /** How many pass the filter - so \"load more\" knows whether the end of the list is the end of the data. */\n total: number\n /** How many I hid - so a hidden conversation is never forgotten. */\n hiddenCount: number\n}\n\nexport interface AssistantSessionAdmin {\n inbox(query: AssistantSessionQuery): Promise<AssistantSessionPage>\n rename(sessionId: string, name: string): Promise<boolean>\n hide(sessionId: string, hidden: boolean): Promise<boolean>\n remove(sessionId: string): Promise<boolean>\n setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>\n}\n\n/** The page size the server allows at most (`chatSessionInbox` clamps to 200). */\nexport const SESSION_PAGE_MAX = 200\n\nasync function mutate(mutation: any, variables: Record<string, unknown>, field: string): Promise<boolean> {\n const result = await client.mutate({ mutation, variables })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n return result.data?.[field] === true\n}\n\n/** The framework's session doors, as they are. */\nexport const assistantSessionAdmin: AssistantSessionAdmin = {\n async inbox({ anchorType, anchorId, search, filter = 'all', filters = {}, limit = 50, offset = 0 }) {\n const hidden = filters.includeHidden ?? filter === 'hidden'\n const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine'\n const result = await client.query({\n query: gql`\n query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean) {\n hiddenSessionCount(anchorType: $t, anchorId: $i)\n chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u)\n chatSessionInbox(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, limit: $l, offset: $o) {\n id\n name\n kind\n lastMessageAt\n lastMessagePreview\n previewRedacted\n unread\n participant\n mine\n owner\n hidden\n visibility\n }\n }\n `,\n variables: {\n t: anchorType,\n i: anchorId,\n /* Hidden ones only when asked - elsewhere they stay out of the list. */\n h: hidden,\n q: search?.trim() || null,\n m: mine,\n k: filters.kind || null,\n p: filters.whose === 'participant' || null,\n v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,\n u: filters.unreadOnly || null,\n l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),\n o: Math.max(0, offset)\n },\n fetchPolicy: 'no-cache'\n })\n\n if (result.errors?.length) throw new Error(result.errors[0].message)\n const entries = result.data?.chatSessionInbox\n if (!Array.isArray(entries)) {\n throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n }\n\n return {\n /* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */\n entries,\n total: result.data?.chatSessionInboxTotal ?? entries.length,\n hiddenCount: result.data?.hiddenSessionCount ?? 0\n }\n },\n\n rename(sessionId, name) {\n return mutate(gql`mutation ($id: String!, $n: String!) { renameAISession(sessionId: $id, name: $n) }`, { id: sessionId, n: name }, 'renameAISession')\n },\n\n hide(sessionId, hidden) {\n return mutate(gql`mutation ($id: String!, $h: Boolean) { hideAISession(sessionId: $id, hidden: $h) }`, { id: sessionId, h: hidden }, 'hideAISession')\n },\n\n remove(sessionId) {\n return mutate(gql`mutation ($id: String!) { deleteAISession(sessionId: $id) }`, { id: sessionId }, 'deleteAISession')\n },\n\n setVisibility(sessionId, visibility) {\n return mutate(\n gql`mutation ($id: String!, $v: String!) { setSessionVisibility(sessionId: $id, visibility: $v) }`,\n { id: sessionId, v: visibility },\n 'setSessionVisibility'\n )\n }\n}\n"]}
@@ -17,6 +17,8 @@ export interface AssistantSessionTarget {
17
17
  scope: string;
18
18
  anchorType: string;
19
19
  anchorId: string;
20
+ /** The conversation kind (mode) the tabs list - all readable conversations of it, not only mine (shared dock §2-6). */
21
+ kind?: string;
20
22
  }
21
23
  export interface AssistantSessionTransport {
22
24
  list(target: AssistantSessionTarget): Promise<AssistantSession[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-session-controller.js","sourceRoot":"","sources":["../../client/utils/assistant-session-controller.ts"],"names":[],"mappings":"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","sourcesContent":["export interface AssistantSession {\n id: string\n name?: string\n /**\n * 어느 대화인가 — 등록된 모드의 kind (ADR-0072 결정 3).\n *\n * 목록은 한 대상의 대화를 **모두** 보여 준다. 그 대상 위에 대화가 둘 설 수 있으므로(A17), 고른 것이\n * 지금 열려 있는 대화와 다를 수 있다. 그때 화면은 그 대화로 따라가야 한다 — 안 그러면 문이 거절하고\n * (`session-kind-mismatch`), 목록은 눌리는데 안 열리는 목록이 된다.\n *\n * 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.\n */\n kind?: string\n}\nexport interface AssistantSessionTarget {\n /** Account/domain boundary supplied by the authenticated host. */\n scope: string\n anchorType: string\n anchorId: string\n}\nexport interface AssistantSessionTransport {\n list(target: AssistantSessionTarget): Promise<AssistantSession[]>\n create(target: AssistantSessionTarget): Promise<AssistantSession>\n isEmpty?(sessionId: string): Promise<boolean>\n /**\n * 이 대화를 여기까지 읽었다 — 열린 대화가 바뀔 때 한 번.\n *\n * 읽은 지점은 **다른 참여자에게 보이는 사실**이다(안 읽음 배지 · 목록의 굵은 글씨). 도크마다 손으로\n * 부르던 것을 여기로 올렸다 — twin 만 부르고 있었고, 멘션으로 사람이 들어올 수 있게 된 뒤에는\n * 세 화면이 같은 사실을 달리 말하게 된다.\n */\n markRead?(sessionId: string): Promise<void> | void\n}\nexport interface AssistantSessionState {\n target?: AssistantSessionTarget\n sessions: AssistantSession[]\n sessionId: string\n loading: boolean\n error: string\n /** Changes on reset, including temporary conversations without a server ID. */\n revision: number\n}\nconst keyOf = (target?: AssistantSessionTarget) => target\n ? JSON.stringify([target.scope, target.anchorType, target.anchorId]) : ''\n\n/** Domain-neutral session lifecycle. Never deletes history or edits host documents. */\nexport class AssistantSessionController {\n state: AssistantSessionState = { sessions: [], sessionId: '', loading: false, error: '', revision: 0 }\n private generation = 0\n private preferred = new Map<string, string>()\n private pending = new Map<string, Promise<AssistantSession[]>>()\n\n private transport: AssistantSessionTransport\n private changed: (state: AssistantSessionState) => void\n private storage?: Pick<Storage, 'getItem' | 'setItem'>\n private lazy: boolean\n private preparing?: Promise<string | undefined>\n constructor(transport: AssistantSessionTransport, changed: (state: AssistantSessionState) => void,\n storage?: Pick<Storage, 'getItem' | 'setItem'>, options: { lazy?: boolean } = {}) {\n this.transport = transport\n this.changed = changed\n this.storage = storage\n this.lazy = !!options.lazy\n }\n\n private remember(key: string, id: string) {\n this.preferred.set(key, id)\n try { this.storage?.setItem('assistant-session:' + key, id) } catch { /* Storage is optional. */ }\n }\n\n private preference(key: string): string | undefined {\n try { return this.preferred.get(key) || this.storage?.getItem('assistant-session:' + key) || undefined }\n catch { return this.preferred.get(key) }\n }\n\n private publish(patch: Partial<AssistantSessionState>) {\n const before = this.state.sessionId\n this.state = { ...this.state, ...patch }\n /*\n * 열린 대화가 바뀌면 읽음 지점을 옮긴다. 한 자리에서 하는 이유는 여는 길이 넷이기 때문이다\n * (열기 · 고르기 · 새로 시작 · 보내며 만들기) — 갈래마다 부르면 한 곳이 빠진다.\n */\n if (this.state.sessionId && this.state.sessionId !== before) void this.markRead(this.state.sessionId)\n this.changed(this.state)\n }\n\n /** 실패는 삼킨다 — 읽음 표시가 못 가는 것이 대화를 막을 이유는 아니다. */\n private async markRead(sessionId: string): Promise<void> {\n try {\n await this.transport.markRead?.(sessionId)\n } catch {\n /* noop */\n }\n }\n\n async open(target?: AssistantSessionTarget, force = false): Promise<void> {\n const key = keyOf(target)\n if (!force && key === keyOf(this.state.target) && (this.state.loading || this.state.sessionId || (this.lazy && this.state.target && !this.state.error))) return\n const generation = ++this.generation\n const snapshot = target ? { ...target } : undefined\n this.publish({ target: snapshot, sessions: [], sessionId: '', loading: !!target, error: '' })\n if (!snapshot) return\n try {\n let pending = this.pending.get(key)\n if (!pending) {\n pending = (async () => {\n const sessions = await this.transport.list(snapshot)\n // Failed/malformed reads must not create replacement sessions.\n if (!Array.isArray(sessions) || sessions.some(session => !session.id)) throw new Error('Invalid session list')\n if (sessions.length || this.lazy) return [...sessions]\n // Do not issue a mutation for a target abandoned while the inbox was loading.\n if (keyOf(this.state.target) !== key || this.pending.get(key) !== pending) throw new Error('Session target changed')\n const created = await this.transport.create(snapshot)\n if (!created?.id) throw new Error('Invalid new session')\n return [created]\n })()\n this.pending.set(key, pending)\n void pending.finally(() => {\n if (this.pending.get(key) === pending) this.pending.delete(key)\n }).catch(() => {})\n }\n const sessions = await pending\n if (generation !== this.generation) return\n const preference = this.preference(key)\n const sessionId = this.lazy && preference === '__draft__' ? '' : sessions.find(session => session.id === preference)?.id || sessions[0]?.id || ''\n this.remember(key, sessionId || '__draft__')\n this.publish({ sessions, sessionId, loading: false })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n select(id: string): boolean {\n if (this.state.loading || !this.state.sessions.some(session => session.id === id)) return false\n this.remember(keyOf(this.state.target), id)\n this.publish({ sessionId: id, error: '' })\n return true\n }\n\n async reset(): Promise<void> {\n if (this.state.loading || this.preparing) return\n if (this.lazy) {\n if (!this.state.sessionId) return\n ++this.generation\n this.remember(keyOf(this.state.target), '__draft__')\n this.publish({ sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const target = this.state.target\n if (!target) {\n this.publish({ sessions: [], sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const generation = ++this.generation\n const key = keyOf(target)\n this.publish({ loading: true, error: '' })\n try {\n if (this.state.sessionId && this.transport.isEmpty) {\n const empty = await this.transport.isEmpty(this.state.sessionId)\n if (generation !== this.generation) return\n if (empty) { this.publish({ loading: false }); return }\n }\n const session = await this.transport.create({ ...target })\n if (!session?.id) throw new Error('Invalid new session')\n this.remember(key, session.id)\n if (generation !== this.generation) return\n this.publish({ sessions: [session, ...this.state.sessions.filter(existing => existing.id !== session.id)], sessionId: session.id,\n loading: false, revision: this.state.revision + 1 })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n /** Called only for a real send. Adoption happens before hosts render the new ID. */\n prepareSession(adopt: (id: string) => void = () => {}): Promise<string | undefined> {\n if (this.state.sessionId) return Promise.resolve(this.state.sessionId)\n if (!this.state.target) return Promise.resolve(undefined)\n if (this.state.loading || this.state.error) return Promise.reject(new Error('Session history is not ready'))\n if (this.preparing) return this.preparing\n const target = { ...this.state.target }, generation = this.generation\n const pending = (async () => {\n const session = await this.transport.create(target)\n if (generation !== this.generation) throw new Error('Session target changed')\n if (!session?.id) throw new Error('Invalid new session')\n adopt(session.id)\n this.remember(keyOf(target), session.id)\n this.publish({ sessionId: session.id, sessions: [session, ...this.state.sessions] })\n return session.id\n })()\n this.preparing = pending\n void pending.finally(() => { if (this.preparing === pending) this.preparing = undefined }).catch(() => {})\n return pending\n }\n\n /** Invalidate pending UI updates at logout/disposal. Server authorization remains authoritative. */\n clear() {\n ++this.generation\n this.preferred.clear()\n this.pending.clear()\n this.publish({ target: undefined, sessions: [], sessionId: '', loading: false, error: '', revision: this.state.revision + 1 })\n }\n}\n"]}
1
+ {"version":3,"file":"assistant-session-controller.js","sourceRoot":"","sources":["../../client/utils/assistant-session-controller.ts"],"names":[],"mappings":"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","sourcesContent":["export interface AssistantSession {\n id: string\n name?: string\n /**\n * 어느 대화인가 — 등록된 모드의 kind (ADR-0072 결정 3).\n *\n * 목록은 한 대상의 대화를 **모두** 보여 준다. 그 대상 위에 대화가 둘 설 수 있으므로(A17), 고른 것이\n * 지금 열려 있는 대화와 다를 수 있다. 그때 화면은 그 대화로 따라가야 한다 — 안 그러면 문이 거절하고\n * (`session-kind-mismatch`), 목록은 눌리는데 안 열리는 목록이 된다.\n *\n * 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.\n */\n kind?: string\n}\nexport interface AssistantSessionTarget {\n /** Account/domain boundary supplied by the authenticated host. */\n scope: string\n anchorType: string\n anchorId: string\n /** The conversation kind (mode) the tabs list - all readable conversations of it, not only mine (shared dock §2-6). */\n kind?: string\n}\nexport interface AssistantSessionTransport {\n list(target: AssistantSessionTarget): Promise<AssistantSession[]>\n create(target: AssistantSessionTarget): Promise<AssistantSession>\n isEmpty?(sessionId: string): Promise<boolean>\n /**\n * 이 대화를 여기까지 읽었다 — 열린 대화가 바뀔 때 한 번.\n *\n * 읽은 지점은 **다른 참여자에게 보이는 사실**이다(안 읽음 배지 · 목록의 굵은 글씨). 도크마다 손으로\n * 부르던 것을 여기로 올렸다 — twin 만 부르고 있었고, 멘션으로 사람이 들어올 수 있게 된 뒤에는\n * 세 화면이 같은 사실을 달리 말하게 된다.\n */\n markRead?(sessionId: string): Promise<void> | void\n}\nexport interface AssistantSessionState {\n target?: AssistantSessionTarget\n sessions: AssistantSession[]\n sessionId: string\n loading: boolean\n error: string\n /** Changes on reset, including temporary conversations without a server ID. */\n revision: number\n}\nconst keyOf = (target?: AssistantSessionTarget) => target\n ? JSON.stringify([target.scope, target.anchorType, target.anchorId]) : ''\n\n/** Domain-neutral session lifecycle. Never deletes history or edits host documents. */\nexport class AssistantSessionController {\n state: AssistantSessionState = { sessions: [], sessionId: '', loading: false, error: '', revision: 0 }\n private generation = 0\n private preferred = new Map<string, string>()\n private pending = new Map<string, Promise<AssistantSession[]>>()\n\n private transport: AssistantSessionTransport\n private changed: (state: AssistantSessionState) => void\n private storage?: Pick<Storage, 'getItem' | 'setItem'>\n private lazy: boolean\n private preparing?: Promise<string | undefined>\n constructor(transport: AssistantSessionTransport, changed: (state: AssistantSessionState) => void,\n storage?: Pick<Storage, 'getItem' | 'setItem'>, options: { lazy?: boolean } = {}) {\n this.transport = transport\n this.changed = changed\n this.storage = storage\n this.lazy = !!options.lazy\n }\n\n private remember(key: string, id: string) {\n this.preferred.set(key, id)\n try { this.storage?.setItem('assistant-session:' + key, id) } catch { /* Storage is optional. */ }\n }\n\n private preference(key: string): string | undefined {\n try { return this.preferred.get(key) || this.storage?.getItem('assistant-session:' + key) || undefined }\n catch { return this.preferred.get(key) }\n }\n\n private publish(patch: Partial<AssistantSessionState>) {\n const before = this.state.sessionId\n this.state = { ...this.state, ...patch }\n /*\n * 열린 대화가 바뀌면 읽음 지점을 옮긴다. 한 자리에서 하는 이유는 여는 길이 넷이기 때문이다\n * (열기 · 고르기 · 새로 시작 · 보내며 만들기) — 갈래마다 부르면 한 곳이 빠진다.\n */\n if (this.state.sessionId && this.state.sessionId !== before) void this.markRead(this.state.sessionId)\n this.changed(this.state)\n }\n\n /** 실패는 삼킨다 — 읽음 표시가 못 가는 것이 대화를 막을 이유는 아니다. */\n private async markRead(sessionId: string): Promise<void> {\n try {\n await this.transport.markRead?.(sessionId)\n } catch {\n /* noop */\n }\n }\n\n async open(target?: AssistantSessionTarget, force = false): Promise<void> {\n const key = keyOf(target)\n if (!force && key === keyOf(this.state.target) && (this.state.loading || this.state.sessionId || (this.lazy && this.state.target && !this.state.error))) return\n const generation = ++this.generation\n const snapshot = target ? { ...target } : undefined\n this.publish({ target: snapshot, sessions: [], sessionId: '', loading: !!target, error: '' })\n if (!snapshot) return\n try {\n let pending = this.pending.get(key)\n if (!pending) {\n pending = (async () => {\n const sessions = await this.transport.list(snapshot)\n // Failed/malformed reads must not create replacement sessions.\n if (!Array.isArray(sessions) || sessions.some(session => !session.id)) throw new Error('Invalid session list')\n if (sessions.length || this.lazy) return [...sessions]\n // Do not issue a mutation for a target abandoned while the inbox was loading.\n if (keyOf(this.state.target) !== key || this.pending.get(key) !== pending) throw new Error('Session target changed')\n const created = await this.transport.create(snapshot)\n if (!created?.id) throw new Error('Invalid new session')\n return [created]\n })()\n this.pending.set(key, pending)\n void pending.finally(() => {\n if (this.pending.get(key) === pending) this.pending.delete(key)\n }).catch(() => {})\n }\n const sessions = await pending\n if (generation !== this.generation) return\n const preference = this.preference(key)\n const sessionId = this.lazy && preference === '__draft__' ? '' : sessions.find(session => session.id === preference)?.id || sessions[0]?.id || ''\n this.remember(key, sessionId || '__draft__')\n this.publish({ sessions, sessionId, loading: false })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n select(id: string): boolean {\n if (this.state.loading || !this.state.sessions.some(session => session.id === id)) return false\n this.remember(keyOf(this.state.target), id)\n this.publish({ sessionId: id, error: '' })\n return true\n }\n\n async reset(): Promise<void> {\n if (this.state.loading || this.preparing) return\n if (this.lazy) {\n if (!this.state.sessionId) return\n ++this.generation\n this.remember(keyOf(this.state.target), '__draft__')\n this.publish({ sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const target = this.state.target\n if (!target) {\n this.publish({ sessions: [], sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const generation = ++this.generation\n const key = keyOf(target)\n this.publish({ loading: true, error: '' })\n try {\n if (this.state.sessionId && this.transport.isEmpty) {\n const empty = await this.transport.isEmpty(this.state.sessionId)\n if (generation !== this.generation) return\n if (empty) { this.publish({ loading: false }); return }\n }\n const session = await this.transport.create({ ...target })\n if (!session?.id) throw new Error('Invalid new session')\n this.remember(key, session.id)\n if (generation !== this.generation) return\n this.publish({ sessions: [session, ...this.state.sessions.filter(existing => existing.id !== session.id)], sessionId: session.id,\n loading: false, revision: this.state.revision + 1 })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n /** Called only for a real send. Adoption happens before hosts render the new ID. */\n prepareSession(adopt: (id: string) => void = () => {}): Promise<string | undefined> {\n if (this.state.sessionId) return Promise.resolve(this.state.sessionId)\n if (!this.state.target) return Promise.resolve(undefined)\n if (this.state.loading || this.state.error) return Promise.reject(new Error('Session history is not ready'))\n if (this.preparing) return this.preparing\n const target = { ...this.state.target }, generation = this.generation\n const pending = (async () => {\n const session = await this.transport.create(target)\n if (generation !== this.generation) throw new Error('Session target changed')\n if (!session?.id) throw new Error('Invalid new session')\n adopt(session.id)\n this.remember(keyOf(target), session.id)\n this.publish({ sessionId: session.id, sessions: [session, ...this.state.sessions] })\n return session.id\n })()\n this.preparing = pending\n void pending.finally(() => { if (this.preparing === pending) this.preparing = undefined }).catch(() => {})\n return pending\n }\n\n /** Invalidate pending UI updates at logout/disposal. Server authorization remains authoritative. */\n clear() {\n ++this.generation\n this.preferred.clear()\n this.pending.clear()\n this.publish({ target: undefined, sessions: [], sessionId: '', loading: false, error: '', revision: this.state.revision + 1 })\n }\n}\n"]}
@@ -16,8 +16,12 @@ export const assistantSessionTransport = {
16
16
  },
17
17
  async list(target) {
18
18
  const result = await client.query({
19
- query: gql `query($type:String!,$id:String!){chatSessionInbox(anchorType:$type,anchorId:$id,mineOnly:true,limit:100){id name kind}}`,
20
- variables: { type: target.anchorType, id: target.anchorId }, fetchPolicy: 'no-cache'
19
+ /*
20
+ * Every readable conversation of this kind on the anchor, not only mine - the same scope the manager
21
+ * shows by default (design/ai/shared-dock.md §2-6). Without a kind: every kind, as before.
22
+ */
23
+ query: gql `query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind}}`,
24
+ variables: { type: target.anchorType, id: target.anchorId, kind: target.kind || null }, fetchPolicy: 'no-cache'
21
25
  });
22
26
  if (result.errors?.length)
23
27
  throw new Error(result.errors[0].message);
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-session-transport.js","sourceRoot":"","sources":["../../client/utils/assistant-session-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,GAAG,MAAM,aAAa,CAAA;AAG7B,6FAA6F;AAC7F,MAAM,CAAC,MAAM,yBAAyB,GAA8B;IAClE,KAAK,CAAC,OAAO,CAAC,SAAS;QACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC,KAAK,EAAE,GAAG,CAAA,6DAA6D;YACvE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU;SACtD,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,mCAAmC,EAAE,CAAC,CAAC,CAAA;QACzK,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAA;IAC9C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,MAAM;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC,KAAK,EAAE,GAAG,CAAA,yHAAyH;YACnI,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,UAAU;SACrF,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,uCAAuC,EAAE,CAAC,CAAC,CAAA;QACjL,OAAO,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAA;IACrC,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,SAAS;QACtB,6DAA6D;QAC7D,MAAM,MAAM,CAAC,MAAM,CAAC;YAClB,QAAQ,EAAE,GAAG,CAAA,uDAAuD;YACpE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;SAC7B,CAAC,CAAA;IACJ,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,MAAM;QACjB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACjC,QAAQ,EAAE,GAAG,CAAA,mGAAmG;YAChH,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE;SAC5D,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,iCAAiC,EAAE,CAAC,CAAC,CAAA;QAC/J,OAAO,MAAM,CAAC,IAAI,CAAC,eAAe,CAAA;IACpC,CAAC;CACF,CAAA","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\nimport type { AssistantSessionTransport } from './assistant-session-controller'\n\n/** Existing authorized session API; does not introduce a product-specific storage format. */\nexport const assistantSessionTransport: AssistantSessionTransport = {\n async isEmpty(sessionId) {\n const result = await client.query({\n query: gql`query($id:String!){chatMessages(sessionId:$id,limit:1){id}}`,\n variables: { id: sessionId }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatMessages)) throw new Error(i18next.t('ai-assistant.text.messages-unreadable', { defaultValue: 'could not read this conversation.' }))\n return result.data.chatMessages.length === 0\n },\n async list(target) {\n const result = await client.query({\n query: gql`query($type:String!,$id:String!){chatSessionInbox(anchorType:$type,anchorId:$id,mineOnly:true,limit:100){id name kind}}`,\n variables: { type: target.anchorType, id: target.anchorId }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatSessionInbox)) throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n return result.data.chatSessionInbox\n },\n async markRead(sessionId) {\n /* 참여자가 아닌 대화(내 읽음 지점이 없는 자리)면 서버가 false 를 돌려준다 — 그것도 답이다. */\n await client.mutate({\n mutation: gql`mutation($id:String!){markSessionRead(sessionId:$id)}`,\n variables: { id: sessionId }\n })\n },\n async create(target) {\n const result = await client.mutate({\n mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name kind}}`,\n variables: { type: target.anchorType, id: target.anchorId }\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!result.data?.createAISession?.id) throw new Error(i18next.t('ai-assistant.text.session-not-created', { defaultValue: 'could not start a conversation.' }))\n return result.data.createAISession\n }\n}\n"]}
1
+ {"version":3,"file":"assistant-session-transport.js","sourceRoot":"","sources":["../../client/utils/assistant-session-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,GAAG,MAAM,aAAa,CAAA;AAG7B,6FAA6F;AAC7F,MAAM,CAAC,MAAM,yBAAyB,GAA8B;IAClE,KAAK,CAAC,OAAO,CAAC,SAAS;QACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC,KAAK,EAAE,GAAG,CAAA,6DAA6D;YACvE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU;SACtD,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,mCAAmC,EAAE,CAAC,CAAC,CAAA;QACzK,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAA;IAC9C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,MAAM;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC;;;eAGG;YACH,KAAK,EAAE,GAAG,CAAA,mIAAmI;YAC7I,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,WAAW,EAAE,UAAU;SAChH,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,uCAAuC,EAAE,CAAC,CAAC,CAAA;QACjL,OAAO,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAA;IACrC,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,SAAS;QACtB,6DAA6D;QAC7D,MAAM,MAAM,CAAC,MAAM,CAAC;YAClB,QAAQ,EAAE,GAAG,CAAA,uDAAuD;YACpE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;SAC7B,CAAC,CAAA;IACJ,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,MAAM;QACjB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACjC,QAAQ,EAAE,GAAG,CAAA,mGAAmG;YAChH,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE;SAC5D,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,iCAAiC,EAAE,CAAC,CAAC,CAAA;QAC/J,OAAO,MAAM,CAAC,IAAI,CAAC,eAAe,CAAA;IACpC,CAAC;CACF,CAAA","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\nimport type { AssistantSessionTransport } from './assistant-session-controller'\n\n/** Existing authorized session API; does not introduce a product-specific storage format. */\nexport const assistantSessionTransport: AssistantSessionTransport = {\n async isEmpty(sessionId) {\n const result = await client.query({\n query: gql`query($id:String!){chatMessages(sessionId:$id,limit:1){id}}`,\n variables: { id: sessionId }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatMessages)) throw new Error(i18next.t('ai-assistant.text.messages-unreadable', { defaultValue: 'could not read this conversation.' }))\n return result.data.chatMessages.length === 0\n },\n async list(target) {\n const result = await client.query({\n /*\n * Every readable conversation of this kind on the anchor, not only mine - the same scope the manager\n * shows by default (design/ai/shared-dock.md §2-6). Without a kind: every kind, as before.\n */\n query: gql`query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind}}`,\n variables: { type: target.anchorType, id: target.anchorId, kind: target.kind || null }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatSessionInbox)) throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n return result.data.chatSessionInbox\n },\n async markRead(sessionId) {\n /* 참여자가 아닌 대화(내 읽음 지점이 없는 자리)면 서버가 false 를 돌려준다 — 그것도 답이다. */\n await client.mutate({\n mutation: gql`mutation($id:String!){markSessionRead(sessionId:$id)}`,\n variables: { id: sessionId }\n })\n },\n async create(target) {\n const result = await client.mutate({\n mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name kind}}`,\n variables: { type: target.anchorType, id: target.anchorId }\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!result.data?.createAISession?.id) throw new Error(i18next.t('ai-assistant.text.session-not-created', { defaultValue: 'could not start a conversation.' }))\n return result.data.createAISession\n }\n}\n"]}