@things-factory/ai-assistant 10.1.26 → 10.1.28

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 (194) hide show
  1. package/client/components/assistant-chat.ts +653 -59
  2. package/client/components/assistant-mode.test.ts +86 -55
  3. package/client/components/assistant-mode.ts +124 -82
  4. package/client/components/chat-echo-dedup.test.ts +23 -0
  5. package/client/components/chat-echo-dedup.ts +29 -3
  6. package/client/components/chat-input-builder.test.ts +20 -20
  7. package/client/components/chat-input-builder.ts +16 -14
  8. package/client/components/chat-proposal-survival.test.ts +73 -0
  9. package/client/components/mention-popup.ts +31 -8
  10. package/client/components/panel-aria.test.ts +34 -0
  11. package/client/components/proposal-outcome-line.ts +33 -0
  12. package/client/utils/assistant-session-controller.test.ts +75 -0
  13. package/client/utils/assistant-session-controller.ts +23 -0
  14. package/client/utils/assistant-session-transport.ts +11 -3
  15. package/dist-client/components/assistant-chat.d.ts +128 -13
  16. package/dist-client/components/assistant-chat.js +613 -67
  17. package/dist-client/components/assistant-chat.js.map +1 -1
  18. package/dist-client/components/assistant-mode.d.ts +80 -68
  19. package/dist-client/components/assistant-mode.js +63 -19
  20. package/dist-client/components/assistant-mode.js.map +1 -1
  21. package/dist-client/components/assistant-mode.test.js +74 -53
  22. package/dist-client/components/assistant-mode.test.js.map +1 -1
  23. package/dist-client/components/chat-echo-dedup.d.ts +2 -0
  24. package/dist-client/components/chat-echo-dedup.js +27 -4
  25. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  26. package/dist-client/components/chat-echo-dedup.test.js +20 -0
  27. package/dist-client/components/chat-echo-dedup.test.js.map +1 -1
  28. package/dist-client/components/chat-input-builder.d.ts +12 -7
  29. package/dist-client/components/chat-input-builder.js +8 -10
  30. package/dist-client/components/chat-input-builder.js.map +1 -1
  31. package/dist-client/components/chat-input-builder.test.js +17 -18
  32. package/dist-client/components/chat-input-builder.test.js.map +1 -1
  33. package/dist-client/components/chat-proposal-survival.test.d.ts +1 -0
  34. package/dist-client/components/chat-proposal-survival.test.js +61 -0
  35. package/dist-client/components/chat-proposal-survival.test.js.map +1 -0
  36. package/dist-client/components/mention-popup.js +28 -8
  37. package/dist-client/components/mention-popup.js.map +1 -1
  38. package/dist-client/components/panel-aria.test.d.ts +1 -0
  39. package/dist-client/components/panel-aria.test.js +31 -0
  40. package/dist-client/components/panel-aria.test.js.map +1 -0
  41. package/dist-client/components/proposal-outcome-line.d.ts +9 -0
  42. package/dist-client/components/proposal-outcome-line.js +15 -0
  43. package/dist-client/components/proposal-outcome-line.js.map +1 -0
  44. package/dist-client/tsconfig.tsbuildinfo +1 -1
  45. package/dist-client/utils/assistant-session-controller.d.ts +10 -0
  46. package/dist-client/utils/assistant-session-controller.js +16 -0
  47. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  48. package/dist-client/utils/assistant-session-controller.test.d.ts +1 -0
  49. package/dist-client/utils/assistant-session-controller.test.js +56 -0
  50. package/dist-client/utils/assistant-session-controller.test.js.map +1 -0
  51. package/dist-client/utils/assistant-session-transport.js +11 -3
  52. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  53. package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
  54. package/dist-server/service/assistant-capability-resolver.js +88 -0
  55. package/dist-server/service/assistant-capability-resolver.js.map +1 -0
  56. package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
  57. package/dist-server/service/assistant-chat-resolver.js +166 -44
  58. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  59. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +25 -0
  60. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +140 -0
  61. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
  62. package/dist-server/service/assistant-mode/index.d.ts +9 -0
  63. package/dist-server/service/assistant-mode/index.js +19 -0
  64. package/dist-server/service/assistant-mode/index.js.map +1 -0
  65. package/dist-server/service/assistant-mode/registry.d.ts +18 -0
  66. package/dist-server/service/assistant-mode/registry.js +136 -0
  67. package/dist-server/service/assistant-mode/registry.js.map +1 -0
  68. package/dist-server/service/assistant-mode/resolve.d.ts +33 -0
  69. package/dist-server/service/assistant-mode/resolve.js +60 -0
  70. package/dist-server/service/assistant-mode/resolve.js.map +1 -0
  71. package/dist-server/service/assistant-mode/types.d.ts +99 -0
  72. package/dist-server/service/assistant-mode/types.js +12 -0
  73. package/dist-server/service/assistant-mode/types.js.map +1 -0
  74. package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
  75. package/dist-server/service/chat-message/carried-turns.js +80 -0
  76. package/dist-server/service/chat-message/carried-turns.js.map +1 -0
  77. package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
  78. package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
  79. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  80. package/dist-server/service/chat-message/chat-message.d.ts +32 -0
  81. package/dist-server/service/chat-message/chat-message.js +17 -0
  82. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  83. package/dist-server/service/chat-message/door-access.d.ts +15 -0
  84. package/dist-server/service/chat-message/door-access.js +26 -0
  85. package/dist-server/service/chat-message/door-access.js.map +1 -0
  86. package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
  87. package/dist-server/service/chat-message/fold-session-history.js +133 -0
  88. package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
  89. package/dist-server/service/chat-message/llm-history.d.ts +7 -0
  90. package/dist-server/service/chat-message/llm-history.js +23 -11
  91. package/dist-server/service/chat-message/llm-history.js.map +1 -1
  92. package/dist-server/service/chat-message/message-doors.d.ts +29 -0
  93. package/dist-server/service/chat-message/message-doors.js +107 -0
  94. package/dist-server/service/chat-message/message-doors.js.map +1 -0
  95. package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
  96. package/dist-server/service/chat-message/proposal-outcome.js +31 -0
  97. package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
  98. package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
  99. package/dist-server/service/chat-message/reader-gate.js +37 -0
  100. package/dist-server/service/chat-message/reader-gate.js.map +1 -0
  101. package/dist-server/service/chat-message/room-message.d.ts +4 -0
  102. package/dist-server/service/chat-message/room-message.js +25 -0
  103. package/dist-server/service/chat-message/room-message.js.map +1 -0
  104. package/dist-server/service/chat-session/chat-session.d.ts +24 -0
  105. package/dist-server/service/chat-session/chat-session.js +26 -2
  106. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  107. package/dist-server/service/chat-session/session-inbox.d.ts +4 -1
  108. package/dist-server/service/chat-session/session-inbox.js +3 -2
  109. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  110. package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
  111. package/dist-server/service/chat-session/session-visibility.js +28 -0
  112. package/dist-server/service/chat-session/session-visibility.js.map +1 -0
  113. package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
  114. package/dist-server/service/chat-session/station-session-guard.js +7 -2
  115. package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
  116. package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
  117. package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
  118. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
  119. package/dist-server/service/chat-session-resolver.d.ts +15 -0
  120. package/dist-server/service/chat-session-resolver.js +267 -15
  121. package/dist-server/service/chat-session-resolver.js.map +1 -1
  122. package/dist-server/service/grounding-sources.d.ts +14 -0
  123. package/dist-server/service/grounding-sources.js +26 -0
  124. package/dist-server/service/grounding-sources.js.map +1 -0
  125. package/dist-server/service/index.d.ts +8 -1
  126. package/dist-server/service/index.js +19 -0
  127. package/dist-server/service/index.js.map +1 -1
  128. package/dist-server/service/request-translator.d.ts +2 -0
  129. package/dist-server/service/request-translator.js +20 -0
  130. package/dist-server/service/request-translator.js.map +1 -0
  131. package/dist-server/service/testing/conversation-script.d.ts +123 -0
  132. package/dist-server/service/testing/conversation-script.js +202 -0
  133. package/dist-server/service/testing/conversation-script.js.map +1 -0
  134. package/dist-server/service/testing/mode-conformance.d.ts +40 -0
  135. package/dist-server/service/testing/mode-conformance.js +90 -0
  136. package/dist-server/service/testing/mode-conformance.js.map +1 -0
  137. package/dist-server/tool-registry-boot-report.d.ts +3 -0
  138. package/dist-server/tool-registry-boot-report.js +25 -2
  139. package/dist-server/tool-registry-boot-report.js.map +1 -1
  140. package/dist-server/tsconfig.tsbuildinfo +1 -1
  141. package/package.json +5 -5
  142. package/server/explicit-graphql-types.test.ts +56 -0
  143. package/server/service/assistant-capability-resolver.ts +66 -0
  144. package/server/service/assistant-chat-resolver.ts +184 -42
  145. package/server/service/assistant-mode/assistant-mode-resolver.ts +102 -0
  146. package/server/service/assistant-mode/index.ts +16 -0
  147. package/server/service/assistant-mode/registry.test.ts +105 -0
  148. package/server/service/assistant-mode/registry.ts +147 -0
  149. package/server/service/assistant-mode/resolve.test.ts +78 -0
  150. package/server/service/assistant-mode/resolve.ts +96 -0
  151. package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
  152. package/server/service/assistant-mode/types.ts +103 -0
  153. package/server/service/chat-message/carried-turns.test.ts +69 -0
  154. package/server/service/chat-message/carried-turns.ts +99 -0
  155. package/server/service/chat-message/chat-message-subscription.ts +20 -4
  156. package/server/service/chat-message/chat-message.ts +44 -0
  157. package/server/service/chat-message/door-access.ts +45 -0
  158. package/server/service/chat-message/fold-session-history.test.ts +98 -0
  159. package/server/service/chat-message/fold-session-history.ts +115 -0
  160. package/server/service/chat-message/llm-history.test.ts +58 -7
  161. package/server/service/chat-message/llm-history.ts +30 -11
  162. package/server/service/chat-message/message-doors.test.ts +87 -0
  163. package/server/service/chat-message/message-doors.ts +117 -0
  164. package/server/service/chat-message/proposal-outcome.test.ts +48 -0
  165. package/server/service/chat-message/proposal-outcome.ts +42 -0
  166. package/server/service/chat-message/reader-gate.ts +42 -0
  167. package/server/service/chat-message/room-message.test.ts +21 -0
  168. package/server/service/chat-message/room-message.ts +22 -0
  169. package/server/service/chat-session/chat-session.ts +31 -1
  170. package/server/service/chat-session/session-inbox.test.ts +18 -0
  171. package/server/service/chat-session/session-inbox.ts +9 -2
  172. package/server/service/chat-session/session-visibility.test.ts +36 -0
  173. package/server/service/chat-session/session-visibility.ts +26 -0
  174. package/server/service/chat-session/station-session-guard.ts +10 -2
  175. package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
  176. package/server/service/chat-session-participant/mention-invite.ts +121 -0
  177. package/server/service/chat-session-resolver.ts +272 -15
  178. package/server/service/grounding-sources.test.ts +55 -0
  179. package/server/service/grounding-sources.ts +46 -0
  180. package/server/service/index.ts +19 -0
  181. package/server/service/request-translator.test.ts +45 -0
  182. package/server/service/request-translator.ts +33 -0
  183. package/server/service/station-session-guard.test.ts +12 -5
  184. package/server/service/testing/conversation-scenario.test.ts +163 -0
  185. package/server/service/testing/conversation-script.test.ts +131 -0
  186. package/server/service/testing/conversation-script.ts +288 -0
  187. package/server/service/testing/mode-conformance.ts +107 -0
  188. package/server/tool-registry-boot-report.ts +25 -2
  189. package/test/assistant-chat-call-sites.test.ts +82 -0
  190. package/translations/en.json +44 -2
  191. package/translations/ja.json +44 -2
  192. package/translations/ko.json +44 -2
  193. package/translations/ms.json +44 -2
  194. package/translations/zh.json +44 -2
@@ -1,97 +1,109 @@
1
1
  /**
2
- * A mode is what a conversation *is*: who the assistant is, what it can do, what the talk is
3
- * about, what it offers to start with, and when it is over.
2
+ * A mode is what a conversation *is*: which conversation this is, and what it is about.
4
3
  *
5
- * Those five used to be four separate properties on the chat element plus one global registry,
6
- * and every host wired them one at a time. Separate means they can disagree, and they did: a
7
- * screen whose `systemPrompt` said "you do not edit boards here" offered `/add-monitor` in the
8
- * same panel, because the prompt was a property and the slash list was global.
9
- *
10
- * So a host declares one value and hands it over. What cannot be split cannot fall out of step.
4
+ * Those two used to be nine. The browser declared the persona, the tool categories, the model and
5
+ * the limits and sent them with every turn, which meant a caller holding the door's privilege
6
+ * chose its own authority and it meant a screen could describe a conversation one way while the
7
+ * server answered it another. Since ADR-0061 decision 7 the app server registers the mode and the
8
+ * browser names it:
11
9
  *
12
10
  * ```ts
13
- * const SPACE_MODE = (spaceId: string): AssistantMode => ({
14
- * id: `twin:space:${spaceId}`,
15
- * systemPrompt: '...',
16
- * toolCategories: ['twin'],
17
- * hostContext: { spaceId },
18
- * slashTemplates: TWIN_SLASH_TEMPLATES
19
- * })
20
- *
21
- * html`<ox-assistant-chat .mode=${SPACE_MODE(this.spaceId)}></ox-assistant-chat>`
11
+ * html`<ox-assistant-chat .mode=${{ kind: 'twin.space', target: spaceId, hostContext: { spaceId } }}></ox-assistant-chat>`
22
12
  * ```
13
+ *
14
+ * What the panel shows — the `/` sentences, the `@` catalog, the two notices — comes from the same
15
+ * declaration, fetched from the server already written in the person's language, so the panel and
16
+ * the door cannot offer different conversations.
23
17
  */
24
18
  export interface AssistantMode {
25
19
  /**
26
- * What this conversation is about, as a string the host can compare.
27
- *
28
- * **The conversation's lifetime hangs on this.** When it changes, the chat closes what is on
29
- * screen and starts again — because a conversation about station 3 is not a conversation
30
- * about station 7, and leaving the old one up means the user reads an answer about a screen
31
- * they are no longer looking at.
32
- *
33
- * Hosts built this by hand before, each in their own way: plant kept a module-global registry
34
- * with subscribers and re-checked the context on every send, twin closed its session when the
35
- * anchor moved. Both were writing the same guard around the same absence.
20
+ * Which conversation this is a `<app>.<surface>` kind the app server registered.
21
+ */
22
+ kind: string;
23
+ /**
24
+ * What it is about: the space, the station, the model being edited.
36
25
  *
37
- * Include whatever makes the subject specific `twin:space:S-12`, `plant:station:3`.
26
+ * **With `kind`, this is the conversation's lifetime.** When either changes the chat closes what
27
+ * is on screen and starts again — a conversation about station 3 is not a conversation about
28
+ * station 7, and leaving the old one up means the person reads an answer about a screen they are
29
+ * no longer looking at.
38
30
  */
39
- id: string;
40
- /** Who the assistant is here, and what it must not claim to do. */
41
- systemPrompt?: string;
42
- /** Which tool categories the server may expose for this conversation. */
43
- toolCategories?: string[];
44
- /** What the host knows about the subject; travels with each request. */
45
- hostContext?: any;
46
- /** Which server-side chat entry point answers. */
47
- chatEndpoint?: 'boardAIChat' | 'assistantChat';
48
- /** What `@` offers. */
49
- catalogEntries?: any[];
31
+ target?: string;
50
32
  /**
51
- * What `/` offers.
33
+ * What the host knows about the subject; travels with each request.
52
34
  *
53
- * These are sentences a person could have typed, not tool names "이 공간의 지금 상태를
54
- * 요약해줘" cannot be generated from `summarizeStatus`. But **a mode must not offer work it
55
- * cannot do**: everything here should be answerable by the tools `toolCategories` opens.
35
+ * **The model never sees this** it reaches the tools as `ctx.host` and nothing else (ADR-0061
36
+ * decision 7). Anything the model must know is answered by a read tool.
56
37
  */
57
- slashTemplates?: any[];
58
- /** The i18n key for the line under an empty conversation. */
59
- footerNoticeKey?: string;
60
- /** Follow-up chips for a proposal that arrived without its own. */
61
- proposalChoices?: any[];
62
- /** The i18n key for the line shown once the host reports a proposal staged. */
63
- stagedNoticeKey?: string;
38
+ hostContext?: any;
39
+ }
40
+ /** What the server said this mode offers, already in the request's locale. */
41
+ export interface AssistantModeView {
42
+ kind: string;
43
+ slashTemplates: any[];
44
+ catalogEntries: any[];
45
+ proposalChoices: any[];
46
+ placeholder?: string;
47
+ footerNotice?: string;
48
+ stagedNotice?: string;
64
49
  }
65
50
  /** True when these two describe different conversations, so the old one should be closed. */
66
51
  export declare function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean;
67
- /** Everything a conversation needs, after the mode, the host's own properties and the
68
- * registered defaults have been reconciled. */
52
+ /** Everything a conversation needs, after the mode, the host's own properties, the registered
53
+ * defaults and the server's answer have been reconciled. */
69
54
  export interface ConversationSettings {
70
- systemPrompt?: string;
71
- toolCategories?: string[];
55
+ /** Set when a mode names this conversation; absent for a host that declares none. */
56
+ kind?: string;
57
+ target?: string;
72
58
  hostContext?: any;
73
59
  chatEndpoint: 'boardAIChat' | 'assistantChat';
74
60
  catalogEntries: any[];
75
61
  slashTemplates: any[];
76
- footerNoticeKey?: string;
62
+ /** What the empty input says. A mode's comes translated from the server. */
63
+ placeholder?: string;
64
+ /** Sentences, not keys — a mode's come translated from the server, a host's from its own i18n. */
65
+ footerNotice?: string;
77
66
  proposalChoices: any[];
78
- stagedNoticeKey?: string;
67
+ stagedNotice?: string;
79
68
  }
80
69
  /**
81
70
  * Reconcile the places a setting can come from.
82
71
  *
83
- * **A host that declares a mode gets the mode and nothing else.** A field the mode leaves out is
84
- * empty: it is not filled from the element's own properties, and it is not filled from the
85
- * defaults another package registered. Filling it back in is how the defect this type removes
86
- * returned — board-ai registers board authoring's `/add-monitor`, its component `@` catalog and
87
- * its "previewed on the board" line globally, and plant's factory mode, which declares no slash
88
- * list, offered them on a factory screen once the board modeller had been loaded. A mode that
89
- * wants those declares them.
72
+ * **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is
73
+ * not filled in from the element's own properties or from the defaults another package registered.
74
+ * Filling it back in is how the defect this type removes returned — board-ai registers board
75
+ * authoring's `/add-monitor`, its component `@` catalog and its "previewed on the board" line
76
+ * globally, and plant's factory mode, which declares no slash list, offered them on a factory
77
+ * screen once the board modeller had been loaded.
78
+ *
79
+ * A mode's offering arrives asynchronously, so until the server has answered a mode offers
80
+ * nothing. Standing the defaults in during that gap would show the board's list for a moment on a
81
+ * screen that does not edit boards.
82
+ *
83
+ * A host that declares no mode gets exactly what it got before modes existed — its own properties,
84
+ * falling back to the registered defaults — and it must name its own door: the `boardAIChat`
85
+ * default was removed with decision 7, because a missing door should show when a surface is
86
+ * declared, not when a person says something.
87
+ */
88
+ export declare function resolveConversation(mode: AssistantMode | undefined, own: Partial<ConversationSettings>, defaults: Partial<ConversationSettings>, view?: AssistantModeView): ConversationSettings;
89
+ /** 한 subject 위에 설 수 있는 대화 하나 — 서버가 준 목록의 한 줄. */
90
+ export interface AssistantModeChoice {
91
+ kind: string;
92
+ label: string;
93
+ isDefault: boolean;
94
+ }
95
+ /**
96
+ * 이 화면을 열 때 어느 대화로 설 것인가 — ADR-0072 A17 ①.
97
+ *
98
+ * ── 왜 기억하나 ───────────────────────────────────────────────────────────
99
+ * 보드를 열 때마다 저작 대화로 되돌아가면, 현장을 물으려고 온 사람은 화면을 열 때마다 같은 손질을
100
+ * 다시 한다. 기억은 이 브라우저에만 남는다 — 서버에 칸을 만들지 않는다.
90
101
  *
91
- * A host that declares no mode gets exactly what it got before modes existed — its own
92
- * properties, falling back to the registered defaults.
102
+ * ── 기억을 언제 버리나 ────────────────────────────────────────────────────
103
+ * 기억한 종류가 지금 목록에 없으면 따르지 않는다. 대화가 하나 없어진 판으로 바뀌었는데 그 이름으로
104
+ * 열려고 하면 대화가 안 열리고, 사람은 자기가 무엇을 잘못했는지 알 길이 없다. 그때는 host 가 준
105
+ * 대화로 둔다.
93
106
  *
94
- * `chatEndpoint` still falls back to `boardAIChat` when nothing names one; that default is a
95
- * separate question (standard A2).
107
+ * 고를 것이 하나뿐이면 기억을 보지 않는다 고른 적이 없는 화면이다.
96
108
  */
97
- export declare function resolveConversation(mode: AssistantMode | undefined, own: Partial<ConversationSettings>, defaults: Partial<ConversationSettings>): ConversationSettings;
109
+ export declare function pickInitialMode(current: string | undefined, choices: ReadonlyArray<AssistantModeChoice>, remembered: string | undefined): string | undefined;
@@ -1,36 +1,80 @@
1
1
  /** True when these two describe different conversations, so the old one should be closed. */
2
2
  export function isDifferentConversation(a, b) {
3
- return (a?.id ?? '') !== (b?.id ?? '');
3
+ return `${a?.kind ?? ''}|${a?.target ?? ''}` !== `${b?.kind ?? ''}|${b?.target ?? ''}`;
4
4
  }
5
5
  /**
6
6
  * Reconcile the places a setting can come from.
7
7
  *
8
- * **A host that declares a mode gets the mode and nothing else.** A field the mode leaves out is
9
- * empty: it is not filled from the element's own properties, and it is not filled from the
10
- * defaults another package registered. Filling it back in is how the defect this type removes
11
- * returned — board-ai registers board authoring's `/add-monitor`, its component `@` catalog and
12
- * its "previewed on the board" line globally, and plant's factory mode, which declares no slash
13
- * list, offered them on a factory screen once the board modeller had been loaded. A mode that
14
- * wants those declares them.
8
+ * **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is
9
+ * not filled in from the element's own properties or from the defaults another package registered.
10
+ * Filling it back in is how the defect this type removes returned — board-ai registers board
11
+ * authoring's `/add-monitor`, its component `@` catalog and its "previewed on the board" line
12
+ * globally, and plant's factory mode, which declares no slash list, offered them on a factory
13
+ * screen once the board modeller had been loaded.
15
14
  *
16
- * A host that declares no mode gets exactly what it got before modes existed — its own
17
- * properties, falling back to the registered defaults.
15
+ * A mode's offering arrives asynchronously, so until the server has answered a mode offers
16
+ * nothing. Standing the defaults in during that gap would show the board's list for a moment on a
17
+ * screen that does not edit boards.
18
18
  *
19
- * `chatEndpoint` still falls back to `boardAIChat` when nothing names one; that default is a
20
- * separate question (standard A2).
19
+ * A host that declares no mode gets exactly what it got before modes existed — its own properties,
20
+ * falling back to the registered defaults — and it must name its own door: the `boardAIChat`
21
+ * default was removed with decision 7, because a missing door should show when a surface is
22
+ * declared, not when a person says something.
21
23
  */
22
- export function resolveConversation(mode, own, defaults) {
23
- const pick = (key) => mode ? mode[key] : (own[key] ?? defaults[key]);
24
+ export function resolveConversation(mode, own, defaults, view) {
25
+ if (mode) {
26
+ /* A view for another kind is the previous conversation's answer arriving late. */
27
+ const offered = view && view.kind === mode.kind ? view : undefined;
28
+ return {
29
+ kind: mode.kind,
30
+ target: mode.target,
31
+ hostContext: mode.hostContext,
32
+ chatEndpoint: 'assistantChat',
33
+ catalogEntries: offered?.catalogEntries ?? [],
34
+ slashTemplates: offered?.slashTemplates ?? [],
35
+ proposalChoices: offered?.proposalChoices ?? [],
36
+ placeholder: offered?.placeholder,
37
+ footerNotice: offered?.footerNotice,
38
+ stagedNotice: offered?.stagedNotice
39
+ };
40
+ }
41
+ const pick = (key) => own[key] ?? defaults[key];
42
+ const chatEndpoint = pick('chatEndpoint');
43
+ if (!chatEndpoint) {
44
+ throw new Error('[ox-assistant-chat] a surface with no mode must name its chatEndpoint. The `boardAIChat` default was ' +
45
+ 'removed (ADR-0061 decision 7) — apps declare a mode and talk to `assistantChat`; the board product ' +
46
+ 'names `boardAIChat` on the element.');
47
+ }
24
48
  return {
25
- systemPrompt: pick('systemPrompt'),
26
- toolCategories: pick('toolCategories'),
27
49
  hostContext: pick('hostContext'),
28
- chatEndpoint: pick('chatEndpoint') ?? 'boardAIChat',
50
+ placeholder: pick('placeholder'),
51
+ chatEndpoint,
29
52
  catalogEntries: pick('catalogEntries') ?? [],
30
53
  slashTemplates: pick('slashTemplates') ?? [],
31
- footerNoticeKey: pick('footerNoticeKey'),
54
+ footerNotice: pick('footerNotice'),
32
55
  proposalChoices: pick('proposalChoices') ?? [],
33
- stagedNoticeKey: pick('stagedNoticeKey')
56
+ stagedNotice: pick('stagedNotice')
34
57
  };
35
58
  }
59
+ /**
60
+ * 이 화면을 열 때 어느 대화로 설 것인가 — ADR-0072 A17 ①.
61
+ *
62
+ * ── 왜 기억하나 ───────────────────────────────────────────────────────────
63
+ * 보드를 열 때마다 저작 대화로 되돌아가면, 현장을 물으려고 온 사람은 화면을 열 때마다 같은 손질을
64
+ * 다시 한다. 기억은 이 브라우저에만 남는다 — 서버에 칸을 만들지 않는다.
65
+ *
66
+ * ── 기억을 언제 버리나 ────────────────────────────────────────────────────
67
+ * 기억한 종류가 지금 목록에 없으면 따르지 않는다. 대화가 하나 없어진 판으로 바뀌었는데 그 이름으로
68
+ * 열려고 하면 대화가 안 열리고, 사람은 자기가 무엇을 잘못했는지 알 길이 없다. 그때는 host 가 준
69
+ * 대화로 둔다.
70
+ *
71
+ * 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다.
72
+ */
73
+ export function pickInitialMode(current, choices, remembered) {
74
+ if (choices.length < 2)
75
+ return current;
76
+ if (!remembered || remembered === current)
77
+ return current;
78
+ return choices.some(choice => choice.kind === remembered) ? remembered : current;
79
+ }
36
80
  //# sourceMappingURL=assistant-mode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-mode.js","sourceRoot":"","sources":["../../client/components/assistant-mode.ts"],"names":[],"mappings":"AA0EA,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,CAAiB,EAAE,CAAiB;IAC1E,OAAO,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC;AAgBD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA+B,EAC/B,GAAkC,EAClC,QAAuC;IAEvC,MAAM,IAAI,GAAG,CAAuC,GAAM,EAA2B,EAAE,CACrF,IAAI,CAAC,CAAC,CAAE,IAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,GAAW,CAAC,GAAG,CAAC,IAAK,QAAgB,CAAC,GAAG,CAAC,CAAC,CAAA;IAE3E,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;QAClC,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC;QACtC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,aAAa;QACnD,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC;QACxC,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE;QAC9C,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC;KACzC,CAAA;AACH,CAAC","sourcesContent":["/**\n * A mode is what a conversation *is*: who the assistant is, what it can do, what the talk is\n * about, what it offers to start with, and when it is over.\n *\n * Those five used to be four separate properties on the chat element plus one global registry,\n * and every host wired them one at a time. Separate means they can disagree, and they did: a\n * screen whose `systemPrompt` said \"you do not edit boards here\" offered `/add-monitor` in the\n * same panel, because the prompt was a property and the slash list was global.\n *\n * So a host declares one value and hands it over. What cannot be split cannot fall out of step.\n *\n * ```ts\n * const SPACE_MODE = (spaceId: string): AssistantMode => ({\n * id: `twin:space:${spaceId}`,\n * systemPrompt: '...',\n * toolCategories: ['twin'],\n * hostContext: { spaceId },\n * slashTemplates: TWIN_SLASH_TEMPLATES\n * })\n *\n * html`<ox-assistant-chat .mode=${SPACE_MODE(this.spaceId)}></ox-assistant-chat>`\n * ```\n */\nexport interface AssistantMode {\n /**\n * What this conversation is about, as a string the host can compare.\n *\n * **The conversation's lifetime hangs on this.** When it changes, the chat closes what is on\n * screen and starts again — because a conversation about station 3 is not a conversation\n * about station 7, and leaving the old one up means the user reads an answer about a screen\n * they are no longer looking at.\n *\n * Hosts built this by hand before, each in their own way: plant kept a module-global registry\n * with subscribers and re-checked the context on every send, twin closed its session when the\n * anchor moved. Both were writing the same guard around the same absence.\n *\n * Include whatever makes the subject specific — `twin:space:S-12`, `plant:station:3`.\n */\n id: string\n\n /** Who the assistant is here, and what it must not claim to do. */\n systemPrompt?: string\n\n /** Which tool categories the server may expose for this conversation. */\n toolCategories?: string[]\n\n /** What the host knows about the subject; travels with each request. */\n hostContext?: any\n\n /** Which server-side chat entry point answers. */\n chatEndpoint?: 'boardAIChat' | 'assistantChat'\n\n /** What `@` offers. */\n catalogEntries?: any[]\n\n /**\n * What `/` offers.\n *\n * These are sentences a person could have typed, not tool names \"이 공간의 지금 상태를\n * 요약해줘\" cannot be generated from `summarizeStatus`. But **a mode must not offer work it\n * cannot do**: everything here should be answerable by the tools `toolCategories` opens.\n */\n slashTemplates?: any[]\n\n /** The i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n\n /** The i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n}\n\n/** True when these two describe different conversations, so the old one should be closed. */\nexport function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {\n return (a?.id ?? '') !== (b?.id ?? '')\n}\n\n/** Everything a conversation needs, after the mode, the host's own properties and the\n * registered defaults have been reconciled. */\nexport interface ConversationSettings {\n systemPrompt?: string\n toolCategories?: string[]\n hostContext?: any\n chatEndpoint: 'boardAIChat' | 'assistantChat'\n catalogEntries: any[]\n slashTemplates: any[]\n footerNoticeKey?: string\n proposalChoices: any[]\n stagedNoticeKey?: string\n}\n\n/**\n * Reconcile the places a setting can come from.\n *\n * **A host that declares a mode gets the mode and nothing else.** A field the mode leaves out is\n * empty: it is not filled from the element's own properties, and it is not filled from the\n * defaults another package registered. Filling it back in is how the defect this type removes\n * returned — board-ai registers board authoring's `/add-monitor`, its component `@` catalog and\n * its \"previewed on the board\" line globally, and plant's factory mode, which declares no slash\n * list, offered them on a factory screen once the board modeller had been loaded. A mode that\n * wants those declares them.\n *\n * A host that declares no mode gets exactly what it got before modes existed — its own\n * properties, falling back to the registered defaults.\n *\n * `chatEndpoint` still falls back to `boardAIChat` when nothing names one; that default is a\n * separate question (standard A2).\n */\nexport function resolveConversation(\n mode: AssistantMode | undefined,\n own: Partial<ConversationSettings>,\n defaults: Partial<ConversationSettings>\n): ConversationSettings {\n const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>\n mode ? (mode as any)[key] : ((own as any)[key] ?? (defaults as any)[key])\n\n return {\n systemPrompt: pick('systemPrompt'),\n toolCategories: pick('toolCategories'),\n hostContext: pick('hostContext'),\n chatEndpoint: pick('chatEndpoint') ?? 'boardAIChat',\n catalogEntries: pick('catalogEntries') ?? [],\n slashTemplates: pick('slashTemplates') ?? [],\n footerNoticeKey: pick('footerNoticeKey'),\n proposalChoices: pick('proposalChoices') ?? [],\n stagedNoticeKey: pick('stagedNoticeKey')\n }\n}\n"]}
1
+ {"version":3,"file":"assistant-mode.js","sourceRoot":"","sources":["../../client/components/assistant-mode.ts"],"names":[],"mappings":"AAqDA,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,CAAiB,EAAE,CAAiB;IAC1E,OAAO,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,CAAA;AACxF,CAAC;AAoBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA+B,EAC/B,GAAkC,EAClC,QAAuC,EACvC,IAAwB;IAExB,IAAI,IAAI,EAAE,CAAC;QACT,kFAAkF;QAClF,MAAM,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;QAClE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,eAAe;YAC7B,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,eAAe,EAAE,OAAO,EAAE,eAAe,IAAI,EAAE;YAC/C,WAAW,EAAE,OAAO,EAAE,WAAW;YACjC,YAAY,EAAE,OAAO,EAAE,YAAY;YACnC,YAAY,EAAE,OAAO,EAAE,YAAY;SACpC,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,CAAuC,GAAM,EAA2B,EAAE,CACpF,GAAW,CAAC,GAAG,CAAC,IAAK,QAAgB,CAAC,GAAG,CAAC,CAAA;IAE7C,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,CAAA;IACzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uGAAuG;YACrG,qGAAqG;YACrG,qCAAqC,CACxC,CAAA;IACH,CAAC;IAED,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,YAAY;QACZ,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;QAClC,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE;QAC9C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;KACnC,CAAA;AACH,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA2B,EAC3B,OAA2C,EAC3C,UAA8B;IAE9B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,OAAO;QAAE,OAAO,OAAO,CAAA;IACzD,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAA;AAClF,CAAC","sourcesContent":["/**\n * A mode is what a conversation *is*: which conversation this is, and what it is about.\n *\n * Those two used to be nine. The browser declared the persona, the tool categories, the model and\n * the limits and sent them with every turn, which meant a caller holding the door's privilege\n * chose its own authority and it meant a screen could describe a conversation one way while the\n * server answered it another. Since ADR-0061 decision 7 the app server registers the mode and the\n * browser names it:\n *\n * ```ts\n * html`<ox-assistant-chat .mode=${{ kind: 'twin.space', target: spaceId, hostContext: { spaceId } }}></ox-assistant-chat>`\n * ```\n *\n * What the panel shows the `/` sentences, the `@` catalog, the two notices comes from the same\n * declaration, fetched from the server already written in the person's language, so the panel and\n * the door cannot offer different conversations.\n */\nexport interface AssistantMode {\n /**\n * Which conversation this is a `<app>.<surface>` kind the app server registered.\n */\n kind: string\n\n /**\n * What it is about: the space, the station, the model being edited.\n *\n * **With `kind`, this is the conversation's lifetime.** When either changes the chat closes what\n * is on screen and starts again — a conversation about station 3 is not a conversation about\n * station 7, and leaving the old one up means the person reads an answer about a screen they are\n * no longer looking at.\n */\n target?: string\n\n /**\n * What the host knows about the subject; travels with each request.\n *\n * **The model never sees this** it reaches the tools as `ctx.host` and nothing else (ADR-0061\n * decision 7). Anything the model must know is answered by a read tool.\n */\n hostContext?: any\n}\n\n/** What the server said this mode offers, already in the request's locale. */\nexport interface AssistantModeView {\n kind: string\n slashTemplates: any[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** True when these two describe different conversations, so the old one should be closed. */\nexport function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {\n return `${a?.kind ?? ''}|${a?.target ?? ''}` !== `${b?.kind ?? ''}|${b?.target ?? ''}`\n}\n\n/** Everything a conversation needs, after the mode, the host's own properties, the registered\n * defaults and the server's answer have been reconciled. */\nexport interface ConversationSettings {\n /** Set when a mode names this conversation; absent for a host that declares none. */\n kind?: string\n target?: string\n hostContext?: any\n chatEndpoint: 'boardAIChat' | 'assistantChat'\n catalogEntries: any[]\n slashTemplates: any[]\n /** What the empty input says. A mode's comes translated from the server. */\n placeholder?: string\n /** Sentences, not keys — a mode's come translated from the server, a host's from its own i18n. */\n footerNotice?: string\n proposalChoices: any[]\n stagedNotice?: string\n}\n\n/**\n * Reconcile the places a setting can come from.\n *\n * **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is\n * not filled in from the element's own properties or from the defaults another package registered.\n * Filling it back in is how the defect this type removes returned — board-ai registers board\n * authoring's `/add-monitor`, its component `@` catalog and its \"previewed on the board\" line\n * globally, and plant's factory mode, which declares no slash list, offered them on a factory\n * screen once the board modeller had been loaded.\n *\n * A mode's offering arrives asynchronously, so until the server has answered a mode offers\n * nothing. Standing the defaults in during that gap would show the board's list for a moment on a\n * screen that does not edit boards.\n *\n * A host that declares no mode gets exactly what it got before modes existed — its own properties,\n * falling back to the registered defaults and it must name its own door: the `boardAIChat`\n * default was removed with decision 7, because a missing door should show when a surface is\n * declared, not when a person says something.\n */\nexport function resolveConversation(\n mode: AssistantMode | undefined,\n own: Partial<ConversationSettings>,\n defaults: Partial<ConversationSettings>,\n view?: AssistantModeView\n): ConversationSettings {\n if (mode) {\n /* A view for another kind is the previous conversation's answer arriving late. */\n const offered = view && view.kind === mode.kind ? view : undefined\n return {\n kind: mode.kind,\n target: mode.target,\n hostContext: mode.hostContext,\n chatEndpoint: 'assistantChat',\n catalogEntries: offered?.catalogEntries ?? [],\n slashTemplates: offered?.slashTemplates ?? [],\n proposalChoices: offered?.proposalChoices ?? [],\n placeholder: offered?.placeholder,\n footerNotice: offered?.footerNotice,\n stagedNotice: offered?.stagedNotice\n }\n }\n\n const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>\n (own as any)[key] ?? (defaults as any)[key]\n\n const chatEndpoint = pick('chatEndpoint')\n if (!chatEndpoint) {\n throw new Error(\n '[ox-assistant-chat] a surface with no mode must name its chatEndpoint. The `boardAIChat` default was ' +\n 'removed (ADR-0061 decision 7) — apps declare a mode and talk to `assistantChat`; the board product ' +\n 'names `boardAIChat` on the element.'\n )\n }\n\n return {\n hostContext: pick('hostContext'),\n placeholder: pick('placeholder'),\n chatEndpoint,\n catalogEntries: pick('catalogEntries') ?? [],\n slashTemplates: pick('slashTemplates') ?? [],\n footerNotice: pick('footerNotice'),\n proposalChoices: pick('proposalChoices') ?? [],\n stagedNotice: pick('stagedNotice')\n }\n}\n\n/** 한 subject 위에 설 수 있는 대화 하나 — 서버가 준 목록의 한 줄. */\nexport interface AssistantModeChoice {\n kind: string\n label: string\n isDefault: boolean\n}\n\n/**\n * 이 화면을 열 때 어느 대화로 설 것인가 — ADR-0072 A17 ①.\n *\n * ── 왜 기억하나 ───────────────────────────────────────────────────────────\n * 보드를 열 때마다 저작 대화로 되돌아가면, 현장을 물으려고 온 사람은 화면을 열 때마다 같은 손질을\n * 다시 한다. 기억은 이 브라우저에만 남는다 — 서버에 칸을 만들지 않는다.\n *\n * ── 기억을 언제 버리나 ────────────────────────────────────────────────────\n * 기억한 종류가 지금 목록에 없으면 따르지 않는다. 대화가 하나 없어진 판으로 바뀌었는데 그 이름으로\n * 열려고 하면 대화가 안 열리고, 사람은 자기가 무엇을 잘못했는지 알 길이 없다. 그때는 host 가 준\n * 대화로 둔다.\n *\n * 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다.\n */\nexport function pickInitialMode(\n current: string | undefined,\n choices: ReadonlyArray<AssistantModeChoice>,\n remembered: string | undefined\n): string | undefined {\n if (choices.length < 2) return current\n if (!remembered || remembered === current) return current\n return choices.some(choice => choice.kind === remembered) ? remembered : current\n}\n"]}
@@ -1,85 +1,106 @@
1
1
  /*
2
- * A mode is one value, and the point of it is that the parts of a conversation cannot
3
- * disagree with each other.
2
+ * A mode is one value, and the point of it is that the parts of a conversation cannot disagree
3
+ * with each other. Since ADR-0061 decision 7 the parts live on the server: the browser names the
4
+ * conversation (`kind`) and its subject (`target`), and the persona, the tool categories, the
5
+ * model and the limits are the app server's.
4
6
  *
5
- * The defect this guards against shipped: a screen whose systemPrompt said "you do not edit
6
- * boards here" offered `/add-monitor` in the same panel, because the prompt was an element
7
- * property and the slash list was a module global. Any resolution that lets a mode supply one
8
- * of them while something else supplies the other brings it back.
7
+ * The defect this still guards against shipped: a screen whose prompt said "you do not edit boards
8
+ * here" offered `/add-monitor` in the same panel, because the prompt was an element property and
9
+ * the slash list was a module global. A mode's wording now comes from the same declaration the
10
+ * door reads, so the two cannot describe different conversations.
9
11
  */
10
- import { isDifferentConversation, resolveConversation } from './assistant-mode';
11
- const MODE = {
12
- id: 'twin:space:S-12',
13
- systemPrompt: 'You are looking at one space. You do not edit boards here.',
14
- toolCategories: ['twin'],
15
- hostContext: { spaceId: 'S-12' },
16
- slashTemplates: [{ name: 'status', template: '이 공간의 지금 상태를 요약해줘' }]
12
+ import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode';
13
+ const MODE = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } };
14
+ /** What the server said this mode offers, already in the request's locale. */
15
+ const MODE_VIEW = {
16
+ kind: 'twin.space',
17
+ slashTemplates: [{ name: 'status', description: 'summarize', template: '이 공간의 지금 상태를 요약해줘' }],
18
+ catalogEntries: [],
19
+ proposalChoices: [],
20
+ footerNotice: undefined,
21
+ stagedNotice: undefined
17
22
  };
23
+ /** The board product, which declares no mode and names its own door. */
18
24
  const HOST_PROPERTIES = {
19
- systemPrompt: 'a prompt the host set the old way',
20
- toolCategories: ['board-ai'],
21
25
  hostContext: { boardId: 'B-1' },
22
26
  chatEndpoint: 'boardAIChat'
23
27
  };
24
28
  const GLOBAL_DEFAULTS = {
25
29
  catalogEntries: [{ type: 'rect' }],
26
30
  slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],
27
- footerNoticeKey: 'text.previewed-on-the-board'
31
+ footerNotice: 'previewed on the board'
28
32
  };
29
- describe('resolveConversation', () => {
30
- test('a mode answers before the host properties it overlaps', () => {
31
- const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS);
32
- expect(c.systemPrompt).toBe(MODE.systemPrompt);
33
- expect(c.toolCategories).toEqual(['twin']);
33
+ describe('a mode names the conversation; the server says what it offers', () => {
34
+ test(' the subject travels, and the door is the neutral one', () => {
35
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW);
36
+ expect(c.kind).toBe('twin.space');
37
+ expect(c.target).toBe('S-12');
34
38
  expect(c.hostContext).toEqual({ spaceId: 'S-12' });
39
+ expect(c.chatEndpoint).toBe('assistantChat');
35
40
  });
36
- test('a mode answers before the global defaults the prompt and the slash list cannot disagree', () => {
37
- const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS);
38
- expect(c.slashTemplates).toEqual(MODE.slashTemplates);
39
- /* A mode that says it does not edit boards must not offer a board edit. */
41
+ test(' what the mode offers comes from the server, not from what another package registered', () => {
42
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW);
43
+ expect(c.slashTemplates).toEqual(MODE_VIEW.slashTemplates);
40
44
  expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor');
41
- });
42
- test('★ what a mode leaves out stays out — the defaults another package registered do not fill it', () => {
43
- const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS);
44
- /* The board's `@` catalog and its "previewed on the board" line belong to board authoring. */
45
45
  expect(c.catalogEntries).toEqual([]);
46
- expect(c.footerNoticeKey).toBeUndefined();
46
+ expect(c.footerNotice).toBeUndefined();
47
47
  });
48
- test('★ a mode that declares no slash list offers none, even when a board package registered its own', () => {
49
- /* plant's factory mode declares no `slashTemplates`; board-ai registers `/add-monitor` globally. */
50
- const { slashTemplates, ...withoutSlash } = MODE;
51
- const c = resolveConversation({ ...withoutSlash, id: 'plant:factory:D-1' }, HOST_PROPERTIES, GLOBAL_DEFAULTS);
48
+ test('★ before the server has answered, a mode offers nothing the defaults do not stand in', () => {
49
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, undefined);
52
50
  expect(c.slashTemplates).toEqual([]);
51
+ expect(c.catalogEntries).toEqual([]);
53
52
  });
54
- test('a mode that leaves out a field the host also set as a property does not take the property either', () => {
55
- const c = resolveConversation({ id: 'twin:none' }, HOST_PROPERTIES, GLOBAL_DEFAULTS);
56
- expect(c.systemPrompt).toBeUndefined();
57
- expect(c.toolCategories).toBeUndefined();
58
- expect(c.hostContext).toBeUndefined();
53
+ test('a view for another kind is ignored the panel is showing this conversation', () => {
54
+ const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, { ...MODE_VIEW, kind: 'twin.board' });
55
+ expect(c.slashTemplates).toEqual([]);
59
56
  });
60
- test('a host with no mode gets exactly what it had before modes existed', () => {
57
+ });
58
+ describe('a host with no mode', () => {
59
+ test('gets its own properties and the registered defaults, as before modes existed', () => {
61
60
  const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS);
62
- expect(c.systemPrompt).toBe(HOST_PROPERTIES.systemPrompt);
63
- expect(c.toolCategories).toEqual(HOST_PROPERTIES.toolCategories);
61
+ expect(c.hostContext).toEqual({ boardId: 'B-1' });
64
62
  expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates);
65
- });
66
- test('nothing anywhere resolves to empty lists and the board endpoint, never undefined', () => {
67
- const c = resolveConversation(undefined, {}, {});
68
- expect(c.catalogEntries).toEqual([]);
69
- expect(c.slashTemplates).toEqual([]);
70
- expect(c.proposalChoices).toEqual([]);
63
+ expect(c.footerNotice).toBe('previewed on the board');
71
64
  expect(c.chatEndpoint).toBe('boardAIChat');
72
65
  });
66
+ test('★ must name its door — there is no default any more', () => {
67
+ expect(() => resolveConversation(undefined, {}, {})).toThrow(/chatEndpoint/);
68
+ });
73
69
  });
74
70
  describe('isDifferentConversation', () => {
75
- const station3 = { id: 'plant:station:3' };
76
- test('a different subject ends the conversation', () => {
77
- expect(isDifferentConversation(station3, { id: 'plant:station:7' })).toBe(true);
71
+ const station3 = { kind: 'plant.station', target: '3' };
72
+ test('a different subject ends the conversation, and so does a different kind', () => {
73
+ expect(isDifferentConversation(station3, { kind: 'plant.station', target: '7' })).toBe(true);
74
+ expect(isDifferentConversation(station3, { kind: 'plant.factory', target: '3' })).toBe(true);
78
75
  expect(isDifferentConversation(undefined, station3)).toBe(true);
79
76
  });
80
- test('rewording the same subject does not', () => {
81
- expect(isDifferentConversation(station3, { ...station3, systemPrompt: 'reworded' })).toBe(false);
77
+ test('the same subject, described again, does not', () => {
78
+ expect(isDifferentConversation(station3, { ...station3, hostContext: { reread: true } })).toBe(false);
82
79
  expect(isDifferentConversation(undefined, undefined)).toBe(false);
83
80
  });
84
81
  });
82
+ /*
83
+ * ADR-0072 A17 ① — 화면을 열 때 어느 대화로 서나.
84
+ *
85
+ * 되살리는 것은 고른 대화이지 세션이 아니다. 그리고 없어진 대화로 되살리지 않는다 — 그러면 대화가
86
+ * 안 열리고 사람은 자기가 무엇을 잘못했는지 알 길이 없다.
87
+ */
88
+ describe('pickInitialMode', () => {
89
+ const choices = [
90
+ { kind: 'twin.board', label: '이 그림 고치기', isDefault: true },
91
+ { kind: 'twin.space', label: '이 현장 지금', isDefault: false }
92
+ ];
93
+ it('★ 지난번에 고른 대화로 돌아간다 — 매번 같은 손질을 다시 하지 않는다', () => {
94
+ expect(pickInitialMode('twin.board', choices, 'twin.space')).toBe('twin.space');
95
+ });
96
+ it('★ 지금 목록에 없는 기억은 따르지 않는다 — 안 열리는 대화로 들어간다', () => {
97
+ expect(pickInitialMode('twin.board', choices, 'twin.gone')).toBe('twin.board');
98
+ });
99
+ it('★ 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다', () => {
100
+ expect(pickInitialMode('plant.station', [choices[0]], 'twin.space')).toBe('plant.station');
101
+ });
102
+ it('기억이 없으면 host 가 준 것으로 선다', () => {
103
+ expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board');
104
+ });
105
+ });
85
106
  //# sourceMappingURL=assistant-mode.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-mode.test.js","sourceRoot":"","sources":["../../client/components/assistant-mode.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A mode is one value, and the point of it is that the parts of a conversation cannot\n * disagree with each other.\n *\n * The defect this guards against shipped: a screen whose systemPrompt said \"you do not edit\n * boards here\" offered `/add-monitor` in the same panel, because the prompt was an element\n * property and the slash list was a module global. Any resolution that lets a mode supply one\n * of them while something else supplies the other brings it back.\n */\nimport { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode'\n\nconst MODE: AssistantMode = {\n id: 'twin:space:S-12',\n systemPrompt: 'You are looking at one space. You do not edit boards here.',\n toolCategories: ['twin'],\n hostContext: { spaceId: 'S-12' },\n slashTemplates: [{ name: 'status', template: '이 공간의 지금 상태를 요약해줘' }]\n}\n\nconst HOST_PROPERTIES = {\n systemPrompt: 'a prompt the host set the old way',\n toolCategories: ['board-ai'],\n hostContext: { boardId: 'B-1' },\n chatEndpoint: 'boardAIChat' as const\n}\n\nconst GLOBAL_DEFAULTS = {\n catalogEntries: [{ type: 'rect' }],\n slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],\n footerNoticeKey: 'text.previewed-on-the-board'\n}\n\ndescribe('resolveConversation', () => {\n test('a mode answers before the host properties it overlaps', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n expect(c.systemPrompt).toBe(MODE.systemPrompt)\n expect(c.toolCategories).toEqual(['twin'])\n expect(c.hostContext).toEqual({ spaceId: 'S-12' })\n })\n\n test('a mode answers before the global defaults the prompt and the slash list cannot disagree', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n expect(c.slashTemplates).toEqual(MODE.slashTemplates)\n /* A mode that says it does not edit boards must not offer a board edit. */\n expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')\n })\n\n test('★ what a mode leaves out stays out — the defaults another package registered do not fill it', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n /* The board's `@` catalog and its \"previewed on the board\" line belong to board authoring. */\n expect(c.catalogEntries).toEqual([])\n expect(c.footerNoticeKey).toBeUndefined()\n })\n\n test('a mode that declares no slash list offers none, even when a board package registered its own', () => {\n /* plant's factory mode declares no `slashTemplates`; board-ai registers `/add-monitor` globally. */\n const { slashTemplates, ...withoutSlash } = MODE\n const c = resolveConversation({ ...withoutSlash, id: 'plant:factory:D-1' }, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n expect(c.slashTemplates).toEqual([])\n })\n\n test('a mode that leaves out a field the host also set as a property does not take the property either', () => {\n const c = resolveConversation({ id: 'twin:none' }, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n expect(c.systemPrompt).toBeUndefined()\n expect(c.toolCategories).toBeUndefined()\n expect(c.hostContext).toBeUndefined()\n })\n\n test('a host with no mode gets exactly what it had before modes existed', () => {\n const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n expect(c.systemPrompt).toBe(HOST_PROPERTIES.systemPrompt)\n expect(c.toolCategories).toEqual(HOST_PROPERTIES.toolCategories)\n expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)\n })\n\n test('nothing anywhere resolves to empty lists and the board endpoint, never undefined', () => {\n const c = resolveConversation(undefined, {}, {})\n expect(c.catalogEntries).toEqual([])\n expect(c.slashTemplates).toEqual([])\n expect(c.proposalChoices).toEqual([])\n expect(c.chatEndpoint).toBe('boardAIChat')\n })\n})\n\ndescribe('isDifferentConversation', () => {\n const station3: AssistantMode = { id: 'plant:station:3' }\n\n test('a different subject ends the conversation', () => {\n expect(isDifferentConversation(station3, { id: 'plant:station:7' })).toBe(true)\n expect(isDifferentConversation(undefined, station3)).toBe(true)\n })\n\n test('rewording the same subject does not', () => {\n expect(isDifferentConversation(station3, { ...station3, systemPrompt: 'reworded' })).toBe(false)\n expect(isDifferentConversation(undefined, undefined)).toBe(false)\n })\n})\n"]}
1
+ {"version":3,"file":"assistant-mode.test.js","sourceRoot":"","sources":["../../client/components/assistant-mode.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A mode is one value, and the point of it is that the parts of a conversation cannot disagree\n * with each other. Since ADR-0061 decision 7 the parts live on the server: the browser names the\n * conversation (`kind`) and its subject (`target`), and the persona, the tool categories, the\n * model and the limits are the app server's.\n *\n * The defect this still guards against shipped: a screen whose prompt said \"you do not edit boards\n * here\" offered `/add-monitor` in the same panel, because the prompt was an element property and\n * the slash list was a module global. A mode's wording now comes from the same declaration the\n * door reads, so the two cannot describe different conversations.\n */\nimport { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode } from './assistant-mode'\n\nconst MODE: AssistantMode = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } }\n\n/** What the server said this mode offers, already in the request's locale. */\nconst MODE_VIEW = {\n kind: 'twin.space',\n slashTemplates: [{ name: 'status', description: 'summarize', template: '이 공간의 지금 상태를 요약해줘' }],\n catalogEntries: [],\n proposalChoices: [],\n footerNotice: undefined,\n stagedNotice: undefined\n}\n\n/** The board product, which declares no mode and names its own door. */\nconst HOST_PROPERTIES = {\n hostContext: { boardId: 'B-1' },\n chatEndpoint: 'boardAIChat' as const\n}\n\nconst GLOBAL_DEFAULTS = {\n catalogEntries: [{ type: 'rect' }],\n slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],\n footerNotice: 'previewed on the board'\n}\n\ndescribe('a mode names the conversation; the server says what it offers', () => {\n test(' the subject travels, and the door is the neutral one', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.kind).toBe('twin.space')\n expect(c.target).toBe('S-12')\n expect(c.hostContext).toEqual({ spaceId: 'S-12' })\n expect(c.chatEndpoint).toBe('assistantChat')\n })\n\n test(' what the mode offers comes from the server, not from what another package registered', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.slashTemplates).toEqual(MODE_VIEW.slashTemplates)\n expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')\n expect(c.catalogEntries).toEqual([])\n expect(c.footerNotice).toBeUndefined()\n })\n\n test('★ before the server has answered, a mode offers nothing — the defaults do not stand in', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, undefined)\n\n expect(c.slashTemplates).toEqual([])\n expect(c.catalogEntries).toEqual([])\n })\n\n test('a view for another kind is ignored the panel is showing this conversation', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, { ...MODE_VIEW, kind: 'twin.board' })\n\n expect(c.slashTemplates).toEqual([])\n })\n})\n\ndescribe('a host with no mode', () => {\n test('gets its own properties and the registered defaults, as before modes existed', () => {\n const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n\n expect(c.hostContext).toEqual({ boardId: 'B-1' })\n expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)\n expect(c.footerNotice).toBe('previewed on the board')\n expect(c.chatEndpoint).toBe('boardAIChat')\n })\n\n test(' must name its door there is no default any more', () => {\n expect(() => resolveConversation(undefined, {}, {})).toThrow(/chatEndpoint/)\n })\n})\n\ndescribe('isDifferentConversation', () => {\n const station3: AssistantMode = { kind: 'plant.station', target: '3' }\n\n test(' a different subject ends the conversation, and so does a different kind', () => {\n expect(isDifferentConversation(station3, { kind: 'plant.station', target: '7' })).toBe(true)\n expect(isDifferentConversation(station3, { kind: 'plant.factory', target: '3' })).toBe(true)\n expect(isDifferentConversation(undefined, station3)).toBe(true)\n })\n\n test('the same subject, described again, does not', () => {\n expect(isDifferentConversation(station3, { ...station3, hostContext: { reread: true } })).toBe(false)\n expect(isDifferentConversation(undefined, undefined)).toBe(false)\n })\n})\n\n/*\n * ADR-0072 A17 ① — 화면을 열 때 어느 대화로 서나.\n *\n * 되살리는 것은 고른 대화이지 세션이 아니다. 그리고 없어진 대화로 되살리지 않는다 — 그러면 대화가\n * 안 열리고 사람은 자기가 무엇을 잘못했는지 알 길이 없다.\n */\ndescribe('pickInitialMode', () => {\n const choices = [\n { kind: 'twin.board', label: '이 그림 고치기', isDefault: true },\n { kind: 'twin.space', label: '이 현장 지금', isDefault: false }\n ]\n\n it(' 지난번에 고른 대화로 돌아간다 — 매번 같은 손질을 다시 하지 않는다', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.space')).toBe('twin.space')\n })\n\n it('★ 지금 목록에 없는 기억은 따르지 않는다 — 안 열리는 대화로 들어간다', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.gone')).toBe('twin.board')\n })\n\n it(' 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다', () => {\n expect(pickInitialMode('plant.station', [choices[0]], 'twin.space')).toBe('plant.station')\n })\n\n it('기억이 없으면 host 가 준 것으로 선다', () => {\n expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board')\n })\n})\n"]}
@@ -22,6 +22,8 @@ export interface RemoteMessage {
22
22
  name?: string;
23
23
  email?: string;
24
24
  } | null;
25
+ /** 이 사람이 이 답의 문을 못 지나 본문이 한 줄로 바뀌어 왔나(ADR-0073 판정 1). */
26
+ redacted?: boolean;
25
27
  }
26
28
  /** 방송 메시지 → 라인. */
27
29
  export declare function remoteMessageToLine(m: RemoteMessage): DedupLine;