@dxos/plugin-assistant 0.13.0 → 0.14.0

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 (234) hide show
  1. package/PLUGIN.mdl +25 -0
  2. package/dist/lib/AssistantCapabilities.mjs +8 -1
  3. package/dist/lib/AssistantCapabilities.mjs.map +1 -1
  4. package/dist/lib/Settings.mjs +4 -0
  5. package/dist/lib/Settings.mjs.map +1 -1
  6. package/dist/lib/TurnReviewSkill.mjs +55 -0
  7. package/dist/lib/TurnReviewSkill.mjs.map +1 -0
  8. package/dist/lib/chunk-AgentArticle.mjs +1 -1
  9. package/dist/lib/chunk-AssistantSettings.mjs +1 -1
  10. package/dist/lib/chunk-AssistantSettings.mjs.map +1 -1
  11. package/dist/lib/chunk-Chat.mjs +322 -174
  12. package/dist/lib/chunk-Chat.mjs.map +1 -1
  13. package/dist/lib/chunk-ChatArticle.mjs +14 -7
  14. package/dist/lib/chunk-ChatArticle.mjs.map +1 -1
  15. package/dist/lib/chunk-ChatDialog.mjs +5 -4
  16. package/dist/lib/chunk-ChatDialog.mjs.map +1 -1
  17. package/dist/lib/chunk-IntegrationPrompt.mjs +1 -1
  18. package/dist/lib/chunk-PLUGIN.mjs +1 -1
  19. package/dist/lib/chunk-PluginUrlPrompt.mjs +1 -1
  20. package/dist/lib/chunk-QuestionSurface.mjs +1 -1
  21. package/dist/lib/chunk-SpaceHomePrompt.mjs +116 -60
  22. package/dist/lib/chunk-SpaceHomePrompt.mjs.map +1 -1
  23. package/dist/lib/chunk-TriggerStatus.mjs +1 -1
  24. package/dist/lib/chunk-agent-service.mjs +3 -1
  25. package/dist/lib/chunk-agent-service.mjs.map +1 -1
  26. package/dist/lib/chunk-analyze-turn.mjs +220 -0
  27. package/dist/lib/chunk-analyze-turn.mjs.map +1 -0
  28. package/dist/lib/{chunk-processor.mjs → chunk-chat-model.mjs} +95 -43
  29. package/dist/lib/chunk-chat-model.mjs.map +1 -0
  30. package/dist/lib/chunk-context.mjs.map +1 -1
  31. package/dist/lib/chunk-create-chat.mjs +5 -19
  32. package/dist/lib/chunk-create-chat.mjs.map +1 -1
  33. package/dist/lib/chunk-create-object.mjs +1 -1
  34. package/dist/lib/chunk-default-skills.mjs +28 -0
  35. package/dist/lib/chunk-default-skills.mjs.map +1 -0
  36. package/dist/lib/chunk-generate-home-suggestions.mjs +8 -6
  37. package/dist/lib/chunk-generate-home-suggestions.mjs.map +1 -1
  38. package/dist/lib/chunk-presets.mjs +1 -1
  39. package/dist/lib/chunk-presets.mjs.map +1 -1
  40. package/dist/lib/chunk-question-resumer.mjs +1 -1
  41. package/dist/lib/chunk-react-surface.mjs +1 -1
  42. package/dist/lib/chunk-run-prompt-in-chat.mjs +2 -1
  43. package/dist/lib/chunk-run-prompt-in-chat.mjs.map +1 -1
  44. package/dist/lib/chunk-run-prompt-in-new-chat.mjs +1 -1
  45. package/dist/lib/chunk-schema-defs.mjs +2 -2
  46. package/dist/lib/chunk-skill-definition.mjs +3 -2
  47. package/dist/lib/chunk-skill-definition.mjs.map +1 -1
  48. package/dist/lib/components.mjs +13 -13
  49. package/dist/lib/components.mjs.map +1 -1
  50. package/dist/lib/hooks.mjs +40 -31
  51. package/dist/lib/hooks.mjs.map +1 -1
  52. package/dist/lib/operations.mjs +3 -1
  53. package/dist/lib/operations.mjs.map +1 -1
  54. package/dist/lib/skills.mjs +2 -1
  55. package/dist/lib/translations.mjs +4 -2
  56. package/dist/lib/translations.mjs.map +1 -1
  57. package/dist/types/src/Chat.d.ts +1 -1
  58. package/dist/types/src/capabilities/agent-service.d.ts.map +1 -1
  59. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -1
  60. package/dist/types/src/{processor/processor.d.ts → chat-model/chat-model.d.ts} +24 -22
  61. package/dist/types/src/chat-model/chat-model.d.ts.map +1 -0
  62. package/dist/types/src/chat-model/chat-model.edge.test.d.ts +2 -0
  63. package/dist/types/src/chat-model/chat-model.edge.test.d.ts.map +1 -0
  64. package/dist/types/src/chat-model/chat-model.node.test.d.ts +2 -0
  65. package/dist/types/src/chat-model/chat-model.node.test.d.ts.map +1 -0
  66. package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts +2 -0
  67. package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts.map +1 -0
  68. package/dist/types/src/{processor → chat-model}/index.d.ts +1 -1
  69. package/dist/types/src/chat-model/index.d.ts.map +1 -0
  70. package/dist/types/src/chat-model/outbox.d.ts.map +1 -0
  71. package/dist/types/src/chat-model/outbox.test.d.ts.map +1 -0
  72. package/dist/types/src/chat-model/presets.d.ts.map +1 -0
  73. package/dist/types/src/chat-model/presets.test.d.ts.map +1 -0
  74. package/dist/types/src/{processor → chat-model}/prompt.d.ts +2 -2
  75. package/dist/types/src/chat-model/prompt.d.ts.map +1 -0
  76. package/dist/types/src/chat-model/prompt.test.d.ts.map +1 -0
  77. package/dist/types/src/chat-model/streaming.node.test.d.ts.map +1 -0
  78. package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts +58 -0
  79. package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts.map +1 -0
  80. package/dist/types/src/chat-model/testing/harness.d.ts +22 -0
  81. package/dist/types/src/chat-model/testing/harness.d.ts.map +1 -0
  82. package/dist/types/src/chat-model/thread.d.ts.map +1 -0
  83. package/dist/types/src/chat-model/thread.test.d.ts.map +1 -0
  84. package/dist/types/src/components/Chat/Chat.d.ts +6 -6
  85. package/dist/types/src/components/Chat/Chat.d.ts.map +1 -1
  86. package/dist/types/src/components/Chat/context.d.ts +5 -3
  87. package/dist/types/src/components/Chat/context.d.ts.map +1 -1
  88. package/dist/types/src/components/ChatPrompt/ChatActions.d.ts +1 -1
  89. package/dist/types/src/components/ChatPrompt/ChatActions.d.ts.map +1 -1
  90. package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts +3 -3
  91. package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts.map +1 -1
  92. package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts +3 -1
  93. package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts.map +1 -1
  94. package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts +4 -3
  95. package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts.map +1 -1
  96. package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts +15 -5
  97. package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts.map +1 -1
  98. package/dist/types/src/components/ChatPrompt/ChatPrompt.stories.d.ts +1 -1
  99. package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts +1 -1
  100. package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts.map +1 -1
  101. package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts +9 -0
  102. package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/Toolbox/Toolbox.d.ts +3 -3
  104. package/dist/types/src/components/Toolbox/Toolbox.d.ts.map +1 -1
  105. package/dist/types/src/containers/ChatArticle/ChatArticle.d.ts.map +1 -1
  106. package/dist/types/src/containers/ChatArticle/ChatArticle.stories.d.ts +1 -1
  107. package/dist/types/src/containers/SpaceHomePrompt/SpaceHomePrompt.d.ts.map +1 -1
  108. package/dist/types/src/hooks/index.d.ts +2 -2
  109. package/dist/types/src/hooks/index.d.ts.map +1 -1
  110. package/dist/types/src/hooks/{useChatProcessor.d.ts → useChatModel.d.ts} +6 -6
  111. package/dist/types/src/hooks/useChatModel.d.ts.map +1 -0
  112. package/dist/types/src/hooks/useDebug.d.ts +3 -3
  113. package/dist/types/src/hooks/useDebug.d.ts.map +1 -1
  114. package/dist/types/src/hooks/useHomeSuggestions.d.ts +3 -3
  115. package/dist/types/src/hooks/useHomeSuggestions.d.ts.map +1 -1
  116. package/dist/types/src/hooks/usePresets.d.ts +1 -1
  117. package/dist/types/src/hooks/usePresets.d.ts.map +1 -1
  118. package/dist/types/src/hooks/useSelectionContext.d.ts +3 -3
  119. package/dist/types/src/hooks/useSelectionContext.d.ts.map +1 -1
  120. package/dist/types/src/operations/analyze-turn.d.ts +5 -0
  121. package/dist/types/src/operations/analyze-turn.d.ts.map +1 -0
  122. package/dist/types/src/operations/analyze-turn.test.d.ts +2 -0
  123. package/dist/types/src/operations/analyze-turn.test.d.ts.map +1 -0
  124. package/dist/types/src/operations/create-chat.d.ts.map +1 -1
  125. package/dist/types/src/operations/generate-home-suggestions.d.ts.map +1 -1
  126. package/dist/types/src/operations/index.d.ts.map +1 -1
  127. package/dist/types/src/operations/run-prompt-in-chat.d.ts.map +1 -1
  128. package/dist/types/src/review/turn-review.d.ts +58 -0
  129. package/dist/types/src/review/turn-review.d.ts.map +1 -0
  130. package/dist/types/src/review/turn-review.test.d.ts +2 -0
  131. package/dist/types/src/review/turn-review.test.d.ts.map +1 -0
  132. package/dist/types/src/skills/index.d.ts +1 -0
  133. package/dist/types/src/skills/index.d.ts.map +1 -1
  134. package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts +21 -0
  135. package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts.map +1 -0
  136. package/dist/types/src/skills/turn-review/index.d.ts +2 -0
  137. package/dist/types/src/skills/turn-review/index.d.ts.map +1 -0
  138. package/dist/types/src/translations.d.ts.map +1 -1
  139. package/dist/types/src/types/Assistant.d.ts +1 -0
  140. package/dist/types/src/types/Assistant.d.ts.map +1 -1
  141. package/dist/types/src/types/AssistantCapabilities.d.ts +7 -1
  142. package/dist/types/src/types/AssistantCapabilities.d.ts.map +1 -1
  143. package/dist/types/src/types/Settings.d.ts +1 -0
  144. package/dist/types/src/types/Settings.d.ts.map +1 -1
  145. package/dist/types/src/util/agent-options.d.ts +27 -0
  146. package/dist/types/src/util/agent-options.d.ts.map +1 -0
  147. package/dist/types/src/util/agent-options.test.d.ts +2 -0
  148. package/dist/types/src/util/agent-options.test.d.ts.map +1 -0
  149. package/dist/types/src/util/default-skills.d.ts +10 -0
  150. package/dist/types/src/util/default-skills.d.ts.map +1 -0
  151. package/dist/types/tsconfig.tsbuildinfo +1 -1
  152. package/package.json +89 -80
  153. package/src/capabilities/agent-service.ts +2 -1
  154. package/src/capabilities/skill-definition.ts +2 -1
  155. package/src/chat-model/chat-model.edge.test.ts +301 -0
  156. package/src/{processor/processor.node.test.ts → chat-model/chat-model.node.test.ts} +19 -9
  157. package/src/chat-model/chat-model.scripted.node.test.ts +441 -0
  158. package/src/{processor/processor.ts → chat-model/chat-model.ts} +140 -45
  159. package/src/{processor → chat-model}/index.ts +1 -1
  160. package/src/{processor → chat-model}/prompt.ts +2 -2
  161. package/src/{processor → chat-model}/streaming.node.test.ts +108 -116
  162. package/src/chat-model/testing/chat-model-recorder.ts +249 -0
  163. package/src/chat-model/testing/harness.ts +81 -0
  164. package/src/{processor → chat-model}/thread.test.ts +1 -1
  165. package/src/components/Chat/Chat.tsx +44 -38
  166. package/src/components/Chat/Error.stories.tsx +1 -1
  167. package/src/components/Chat/context.ts +5 -3
  168. package/src/components/ChatPrompt/ChatActions.tsx +1 -1
  169. package/src/components/ChatPrompt/ChatMcpErrors.tsx +6 -6
  170. package/src/components/ChatPrompt/ChatOptions.stories.tsx +37 -3
  171. package/src/components/ChatPrompt/ChatOptions.tsx +118 -10
  172. package/src/components/ChatPrompt/ChatPrompt.stories.tsx +10 -10
  173. package/src/components/ChatPrompt/ChatPrompt.tsx +44 -27
  174. package/src/components/ChatPrompt/ChatStatus.tsx +8 -7
  175. package/src/components/Toolbox/Toolbox.tsx +12 -13
  176. package/src/containers/AssistantSettings/AssistantSettings.tsx +1 -1
  177. package/src/containers/ChatArticle/ChatArticle.stories.tsx +1 -1
  178. package/src/containers/ChatArticle/ChatArticle.tsx +16 -6
  179. package/src/containers/ChatDialog/ChatDialog.tsx +4 -4
  180. package/src/containers/SpaceHomePrompt/SpaceHomePrompt.tsx +110 -57
  181. package/src/hooks/index.ts +2 -2
  182. package/src/hooks/{useChatProcessor.ts → useChatModel.ts} +16 -14
  183. package/src/hooks/useDebug.ts +8 -8
  184. package/src/hooks/useHomeSuggestions.ts +14 -9
  185. package/src/hooks/usePresets.ts +2 -2
  186. package/src/hooks/useProcessEphemeralStatus.ts +1 -1
  187. package/src/hooks/useSelectionContext.ts +3 -5
  188. package/src/operations/analyze-turn.test.ts +194 -0
  189. package/src/operations/analyze-turn.ts +96 -0
  190. package/src/operations/create-chat.ts +4 -36
  191. package/src/operations/generate-home-suggestions.test.ts +1 -2
  192. package/src/operations/generate-home-suggestions.ts +17 -9
  193. package/src/operations/index.ts +2 -0
  194. package/src/operations/run-prompt-in-chat.ts +3 -2
  195. package/src/review/turn-review.test.ts +92 -0
  196. package/src/review/turn-review.ts +213 -0
  197. package/src/skills/index.ts +1 -0
  198. package/src/skills/turn-review/TurnReviewSkill.ts +46 -0
  199. package/src/skills/turn-review/index.ts +5 -0
  200. package/src/translations.ts +2 -0
  201. package/src/types/AssistantCapabilities.ts +8 -1
  202. package/src/types/Settings.ts +7 -0
  203. package/src/util/agent-options.test.ts +54 -0
  204. package/src/util/agent-options.ts +60 -0
  205. package/src/util/default-skills.ts +35 -0
  206. package/dist/lib/chunk-processor.mjs.map +0 -1
  207. package/dist/types/src/hooks/useChatProcessor.d.ts.map +0 -1
  208. package/dist/types/src/processor/index.d.ts.map +0 -1
  209. package/dist/types/src/processor/outbox.d.ts.map +0 -1
  210. package/dist/types/src/processor/outbox.test.d.ts.map +0 -1
  211. package/dist/types/src/processor/presets.d.ts.map +0 -1
  212. package/dist/types/src/processor/presets.test.d.ts.map +0 -1
  213. package/dist/types/src/processor/processor.d.ts.map +0 -1
  214. package/dist/types/src/processor/processor.node.test.d.ts +0 -2
  215. package/dist/types/src/processor/processor.node.test.d.ts.map +0 -1
  216. package/dist/types/src/processor/prompt.d.ts.map +0 -1
  217. package/dist/types/src/processor/prompt.test.d.ts.map +0 -1
  218. package/dist/types/src/processor/streaming.node.test.d.ts.map +0 -1
  219. package/dist/types/src/processor/thread.d.ts.map +0 -1
  220. package/dist/types/src/processor/thread.test.d.ts.map +0 -1
  221. /package/dist/types/src/{processor → chat-model}/outbox.d.ts +0 -0
  222. /package/dist/types/src/{processor → chat-model}/outbox.test.d.ts +0 -0
  223. /package/dist/types/src/{processor → chat-model}/presets.d.ts +0 -0
  224. /package/dist/types/src/{processor → chat-model}/presets.test.d.ts +0 -0
  225. /package/dist/types/src/{processor → chat-model}/prompt.test.d.ts +0 -0
  226. /package/dist/types/src/{processor → chat-model}/streaming.node.test.d.ts +0 -0
  227. /package/dist/types/src/{processor → chat-model}/thread.d.ts +0 -0
  228. /package/dist/types/src/{processor → chat-model}/thread.test.d.ts +0 -0
  229. /package/src/{processor → chat-model}/outbox.test.ts +0 -0
  230. /package/src/{processor → chat-model}/outbox.ts +0 -0
  231. /package/src/{processor → chat-model}/presets.test.ts +0 -0
  232. /package/src/{processor → chat-model}/presets.ts +0 -0
  233. /package/src/{processor → chat-model}/prompt.test.ts +0 -0
  234. /package/src/{processor → chat-model}/thread.ts +0 -0
@@ -2,11 +2,14 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { useAtomValue } from '@effect/atom-react/Hooks';
6
+ import * as Atom from 'effect/reactivity/Atom';
5
7
  import React, { type JSX, useCallback, useMemo, useState } from 'react';
6
8
 
7
- import { Provider } from '@dxos/ai';
9
+ import { Provider, SessionConfig } from '@dxos/ai';
8
10
  import * as Hooks from '@dxos/app-framework/Hooks';
9
11
  import { type AiContext } from '@dxos/assistant';
12
+ import * as Agent from '@dxos/assistant/Agent';
10
13
  import type * as Chat from '@dxos/assistant/Chat';
11
14
  import * as McpServer from '@dxos/compute/McpServer';
12
15
  import { type Database, Filter, Obj, Ref, type Registry, Type, URI } from '@dxos/echo';
@@ -19,6 +22,7 @@ import { SearchList, useSearchListResults } from '@dxos/react-ui-search';
19
22
  import * as Button from '@dxos/react-ui/Button';
20
23
  import * as Field from '@dxos/react-ui/Field';
21
24
  import * as UiHooks from '@dxos/react-ui/Hooks';
25
+ import * as Icon from '@dxos/react-ui/Icon';
22
26
  import * as Input from '@dxos/react-ui/Input';
23
27
  import * as Layout from '@dxos/react-ui/Layout';
24
28
  import * as Popover from '@dxos/react-ui/Popover';
@@ -41,7 +45,8 @@ import {
41
45
  import { meta } from '#meta';
42
46
  import { Assistant, AssistantCapabilities, AssistantPreset } from '#types';
43
47
 
44
- import { resolveProvider } from '../../processor/index.ts';
48
+ import { resolveProvider } from '../../chat-model/index.ts';
49
+ import { agentOptions } from '../../util/agent-options.ts';
45
50
 
46
51
  const styles = {
47
52
  panel: 'w-[calc(100dvw-.5rem)] sm:w-max max-w-document-width',
@@ -52,12 +57,23 @@ export type ChatOptionsProps = AssistantPreset.ChatPresetProps & {
52
57
  chat?: Chat.Chat;
53
58
  context?: AiContext.Binder;
54
59
  registry?: Registry.Registry;
60
+ /** Whether the conversation has begun; its agent is fixed from then on. */
61
+ started?: boolean;
55
62
  };
56
63
 
57
64
  /**
58
65
  * Manages the runtime context for the chat.
59
66
  */
60
- export const ChatOptions = ({ db, chat, context, registry, presets, preset, onPresetChange }: ChatOptionsProps) => {
67
+ export const ChatOptions = ({
68
+ db,
69
+ chat,
70
+ context,
71
+ registry,
72
+ started,
73
+ presets,
74
+ preset,
75
+ onPresetChange,
76
+ }: ChatOptionsProps) => {
61
77
  const { t } = UiHooks.useTranslation(meta.profile.key);
62
78
 
63
79
  return (
@@ -91,7 +107,15 @@ export const ChatOptions = ({ db, chat, context, registry, presets, preset, onPr
91
107
  </Popover.Trigger>
92
108
  <Popover.Content classNames={styles.panel}>
93
109
  {/* No Body: each tab's list and the tab bar share the popover's own padding. */}
94
- <Tabs.Root orientation='horizontal' defaultValue='view' classNames='grid grid-rows-[1fr_40px] w-full'>
110
+ {/* A chat that has not begun opens on its agent, since that is the choice it is about to lose. */}
111
+ <Tabs.Root
112
+ orientation='horizontal'
113
+ defaultValue={started ? 'view' : 'agent'}
114
+ classNames='grid grid-rows-[1fr_40px] w-full'
115
+ >
116
+ <Tabs.Content tabIndex={-1} classNames='dx-focus-ring-inset overflow-hidden' value='agent'>
117
+ <AgentPanel chat={chat} started={started} />
118
+ </Tabs.Content>
95
119
  <Tabs.Content tabIndex={-1} classNames='dx-focus-ring-inset overflow-hidden' value='view'>
96
120
  <ViewPanel chat={chat} />
97
121
  </Tabs.Content>
@@ -108,6 +132,12 @@ export const ChatOptions = ({ db, chat, context, registry, presets, preset, onPr
108
132
  <EnvironmentPanel chat={chat} />
109
133
  </Tabs.Content>
110
134
  <Tabs.List classNames='p-1 gap-1 border-t border-separator'>
135
+ <Tabs.Trigger
136
+ value='agent'
137
+ icon='ph--robot--regular'
138
+ label={t('options.agent.title')}
139
+ data-testid='assistant.options.agent'
140
+ />
111
141
  <Tabs.Trigger value='view' icon='ph--eye--regular' label={t('chat-view.title')} />
112
142
  <Tabs.Trigger value='skills' icon='ph--student--regular' label={t('options.skills.title')} />
113
143
  <Tabs.Trigger value='mcp-servers' icon='ph--plugs-connected--regular' label={t('options.mcp.title')} />
@@ -126,6 +156,77 @@ export const ChatOptions = ({ db, chat, context, registry, presets, preset, onPr
126
156
  </Tabs.Root>
127
157
  </Popover.Content>
128
158
  </Popover.Root>
159
+
160
+ <AgentBadge chat={chat} />
161
+ </Layout.Flex>
162
+ );
163
+ };
164
+
165
+ /** The chat's agent and the picker's rows, live with each agent's availability. */
166
+ const useChatAgent = (chat: Chat.Chat | undefined) => {
167
+ const agents = Hooks.useOptionalCapabilities(AssistantCapabilities.Agent);
168
+ const availabilityAtom = useMemo(() => Atom.make((get) => agents.map((agent) => get(agent.availability))), [agents]);
169
+ const availability = useAtomValue(availabilityAtom);
170
+ const [session] = useObject(chat, 'session');
171
+ const current = SessionConfig.harnessOf(session);
172
+ const options = useMemo(() => agentOptions({ agents, availability, current }), [agents, availability, current]);
173
+ return { current, options };
174
+ };
175
+
176
+ /**
177
+ * Which agent runs this chat. Picked before the first message only: an agent keeps its own session
178
+ * with the model, so one switched in mid-conversation would answer without the turns before it.
179
+ */
180
+ const AgentPanel = ({ chat, started }: Pick<ChatOptionsProps, 'chat' | 'started'>) => {
181
+ const { t } = UiHooks.useTranslation(meta.profile.key);
182
+ const { current, options } = useChatAgent(chat);
183
+ const handleChange = useCallback(
184
+ (harness: string) => {
185
+ if (chat) {
186
+ Obj.update(chat, (chat) => {
187
+ chat.session = { ...chat.session, harness };
188
+ });
189
+ }
190
+ },
191
+ [chat],
192
+ );
193
+
194
+ return (
195
+ <Layout.Flex column classNames='dx-expand' data-testid='assistant.agents'>
196
+ <Listbox.Root value={current} onValueChange={handleChange} disabled={!chat || started} autoFocus items={options}>
197
+ <Listbox.Content gutter='none' aria-label={t('options.agent.title')}>
198
+ {options.map(({ value }) => (
199
+ <Listbox.Item
200
+ key={value}
201
+ id={value}
202
+ classNames='dx-focus-ring rounded-xs'
203
+ data-testid={`assistant.agents.${value}`}
204
+ />
205
+ ))}
206
+ </Listbox.Content>
207
+ </Listbox.Root>
208
+ {started && <p className='px-form-chrome py-1 text-xs text-fg-muted'>{t('agent-locked.message')}</p>}
209
+ </Layout.Flex>
210
+ );
211
+ };
212
+
213
+ /** Names the agent beside the options when it is not Composer's own, so a reader knows who answers. */
214
+ const AgentBadge = ({ chat }: Pick<ChatOptionsProps, 'chat'>) => {
215
+ const { current, options } = useChatAgent(chat);
216
+ const agent = options.find(({ value }) => value === current);
217
+ if (!agent || agent.value === SessionConfig.COMPOSER_HARNESS) {
218
+ return null;
219
+ }
220
+
221
+ return (
222
+ <Layout.Flex
223
+ align='center'
224
+ gap='xs'
225
+ classNames='text-xs text-fg-muted whitespace-nowrap'
226
+ data-testid='assistant.agent'
227
+ >
228
+ <Icon.Icon icon={agent.icon} size='sm' />
229
+ <span>{agent.label}</span>
129
230
  </Layout.Flex>
130
231
  );
131
232
  };
@@ -152,7 +253,7 @@ const SkillsPanel = ({ registry, db, context }: Pick<ChatOptionsProps, 'registry
152
253
  const isActive = activeSkills.has(skillId);
153
254
  return (
154
255
  <SearchList.Item
155
- classNames='flex items-center overflow-hidden'
256
+ classNames='overflow-hidden'
156
257
  key={skillId}
157
258
  value={skillId}
158
259
  label={skill.name}
@@ -211,10 +312,17 @@ const EnvironmentPanel = ({ chat }: Pick<ChatOptionsProps, 'chat'>) => {
211
312
  // Offered only where an edge service is configured, which is the same condition that decides
212
313
  // whether `RemoteProcessManager` is the real manager or `layerNoop`: against the noop a spawn has
213
314
  // no `list` or `spawn`, so choosing `remote` would persist a flag the next prompt cannot honour.
214
- const environments = client?.config.values.runtime?.services?.edge?.url
215
- ? CHAT_ENVIRONMENTS
216
- : CHAT_ENVIRONMENTS.filter((environment) => environment !== 'remote');
217
- const value: ChatEnvironment = remote ? 'remote' : 'local';
315
+ // An agent's chat runs on EDGE (`Agent.chatLocation`) unless explicitly kept local: with EDGE configured local is
316
+ // no choice there, and without it local stays offered so the chat can be moved somewhere it can run.
317
+ const edgeConfigured = Boolean(client?.config.values.runtime?.services?.edge?.url);
318
+ const agentOnEdge = chat !== undefined && Agent.isAgentChat(chat) && chat.remote !== false;
319
+ const environments =
320
+ agentOnEdge && edgeConfigured
321
+ ? CHAT_ENVIRONMENTS.filter((environment) => environment === 'remote')
322
+ : agentOnEdge || edgeConfigured
323
+ ? CHAT_ENVIRONMENTS
324
+ : CHAT_ENVIRONMENTS.filter((environment) => environment !== 'remote');
325
+ const value: ChatEnvironment = remote || agentOnEdge ? 'remote' : 'local';
218
326
  const handleChange = useCallback((value: string) => setRemote(value === 'remote'), [setRemote]);
219
327
 
220
328
  return (
@@ -608,7 +716,7 @@ export const ObjectsPanel = ({ db, context }: Pick<ChatOptionsProps, 'db' | 'con
608
716
  const styles = hue ? getStyles(hue) : undefined;
609
717
  return (
610
718
  <SearchList.Item
611
- classNames='flex items-center overflow-hidden'
719
+ classNames='overflow-hidden'
612
720
  key={object.id}
613
721
  value={object.id}
614
722
  icon={icon}
@@ -24,7 +24,7 @@ import { useRegistry, useSpaces } from '@dxos/react-client/echo';
24
24
  import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
25
25
  import { Message, Task } from '@dxos/types';
26
26
 
27
- import { useChatProcessor, useChatServices, usePresets } from '#hooks';
27
+ import { useChatModel, useChatServices, usePresets } from '#hooks';
28
28
  import { AssistantPlugin } from '#plugin';
29
29
  import { translations } from '#translations';
30
30
  import { AssistantCapabilities } from '#types';
@@ -38,7 +38,7 @@ type StoryArgs = {
38
38
  /** Prompts already queued on the feed, as a submit-while-busy leaves them. */
39
39
  queued?: string[];
40
40
  /**
41
- * Holds the processor `active` while leaving `streaming` false — a turn parked in a tool call.
41
+ * Holds the chat model `active` while leaving `streaming` false — a turn parked in a tool call.
42
42
  * This is the state the send/stop control has to get right, and no scripted model can hold it
43
43
  * still long enough to assert against.
44
44
  */
@@ -55,26 +55,26 @@ const DefaultStory = ({ tasksVisible: initialTasksVisible, running }: StoryArgs)
55
55
  const { preset, ...chatProps } = usePresets(settings, chat);
56
56
  const db = space?.db;
57
57
  const runtime = useChatServices({ id: db?.spaceId });
58
- const processor = useChatProcessor({ db, chat, preset, runtime, registry, settings });
58
+ const chatModel = useChatModel({ db, chat, preset, runtime, registry, settings });
59
59
 
60
60
  // Drives the control's mode from the outside, so the assertion does not race a real turn. The
61
- // processor reads its atoms from the ambient registry, which is what the story writes to.
62
- // The same registry `useChatProcessor` hands the processor, so a write here is a write it sees.
61
+ // chat model reads its atoms from the ambient registry, which is what the story writes to.
62
+ // The same registry `useChatModel` hands the chat model, so a write here is a write it sees.
63
63
  const atomRegistry = useContext(RegistryContext);
64
64
  useEffect(() => {
65
- if (running && processor) {
66
- atomRegistry.set(processor.active, true);
65
+ if (running && chatModel) {
66
+ atomRegistry.set(chatModel.active, true);
67
67
  }
68
- }, [running, processor, atomRegistry]);
68
+ }, [running, chatModel, atomRegistry]);
69
69
 
70
- if (!chat || !db || !processor) {
70
+ if (!chat || !db || !chatModel) {
71
71
  return <Loading />;
72
72
  }
73
73
 
74
74
  return (
75
75
  <div className='flex justify-center p-4'>
76
76
  <div className='w-full max-w-document-width'>
77
- <Chat.Root chat={chat} db={db} processor={processor}>
77
+ <Chat.Root chat={chat} db={db} chatModel={chatModel}>
78
78
  <Chat.Status classNames='px-3 rounded-sm bg-group-surface' />
79
79
  {/* `attendableId` is the graph node contributed actions are filed under; the story's chat
80
80
  has no node, so the row shows only its own controls unless a plugin renders one. The
@@ -8,10 +8,11 @@ import * as Option from 'effect/Option';
8
8
  import type * as Atom from 'effect/reactivity/Atom';
9
9
  import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
10
10
 
11
+ import { type AiContext } from '@dxos/assistant';
11
12
  import type * as Chat from '@dxos/assistant/Chat';
12
13
  import { type Event } from '@dxos/async';
13
14
  import * as Project from '@dxos/compute/Project';
14
- import { type Database, Obj } from '@dxos/echo';
15
+ import { type Database, Obj, type Registry } from '@dxos/echo';
15
16
  import { useObject } from '@dxos/echo-react';
16
17
  import {
17
18
  ChatEditor,
@@ -22,17 +23,17 @@ import {
22
23
  } from '@dxos/react-ui-chat';
23
24
  import type { ActionGraphProps } from '@dxos/react-ui-menu';
24
25
  import * as Hooks from '@dxos/react-ui/Hooks';
26
+ import * as Layout from '@dxos/react-ui/Layout';
25
27
  import type * as Util from '@dxos/react-ui/Util';
26
28
  import { pendingText } from '@dxos/ui-editor';
27
- import { mx } from '@dxos/ui-theme';
28
29
  import { type Merge } from '@dxos/util';
29
30
 
30
31
  import { useChatKeymapExtensions } from '#hooks';
31
32
  import { meta } from '#meta';
32
33
  import { AssistantPreset } from '#types';
33
34
 
35
+ import { type ChatModel, getChatModelState } from '../../chat-model/index.ts';
34
36
  import { TaskSlashCommands } from '../../commands/index.ts';
35
- import { type AiChatProcessor, getProcessorState } from '../../processor/index.ts';
36
37
  import { type ChatEvent } from '../Chat/index.ts';
37
38
  import { ChatActions, type ChatActionsProps } from './ChatActions.tsx';
38
39
  import { ChatMcpErrors } from './ChatMcpErrors.tsx';
@@ -50,8 +51,15 @@ export type ChatPromptProps = Merge<
50
51
  expandable?: boolean;
51
52
  db?: Database.Database;
52
53
  chat?: Chat.Chat;
53
- /** Undefined while the processor is still opening: the prompt takes text but holds it until then. */
54
- processor?: AiChatProcessor;
54
+ /** Undefined while the chat model is still opening: the prompt takes text but holds it until then. */
55
+ chatModel?: ChatModel;
56
+ /**
57
+ * Bindings for a draft with no chat model, whose caller handles the submit event: backs the context
58
+ * controls and lets the prompt send.
59
+ */
60
+ context?: AiContext.Binder;
61
+ /** Skill registry for the context controls when there is no chat model. */
62
+ registry?: Registry.Registry;
55
63
  event: Event<ChatEvent>;
56
64
  /** Whether the checklist beside the prompt is shown; the toggle renders only when provided. */
57
65
  tasksVisible?: boolean;
@@ -71,6 +79,8 @@ export type ChatPromptProps = Merge<
71
79
  queueSize?: number;
72
80
  /** The most prompts that may wait behind a running turn; past it the prompt takes no more until one is taken up. */
73
81
  maxQueue?: number;
82
+ /** Whether the conversation has begun, which fixes its agent. */
83
+ started?: boolean;
74
84
  /** Object the chat is attached to; its project instructions (if any) supply sentinel-command completion. */
75
85
  companionTo?: Obj.Unknown;
76
86
  }>,
@@ -82,7 +92,9 @@ export const ChatPrompt = ({
82
92
  outline,
83
93
  db,
84
94
  chat,
85
- processor,
95
+ chatModel,
96
+ context: draftContext,
97
+ registry: draftRegistry,
86
98
  event,
87
99
  tasksVisible,
88
100
  attendableId,
@@ -96,13 +108,16 @@ export const ChatPrompt = ({
96
108
  presets,
97
109
  preset,
98
110
  companionTo,
111
+ started,
99
112
  onPresetChange,
100
113
  }: ChatPromptProps) => {
101
114
  const { t } = Hooks.useTranslation(meta.profile.key);
102
- const processorState = getProcessorState(processor);
103
- const error = useAtomValue(processorState.error).pipe(Option.getOrUndefined);
104
- const streaming = useAtomValue(processorState.streaming);
105
- const active = useAtomValue(processorState.active);
115
+ const chatModelState = getChatModelState(chatModel);
116
+ const error = useAtomValue(chatModelState.error).pipe(Option.getOrUndefined);
117
+ const streaming = useAtomValue(chatModelState.streaming);
118
+ const active = useAtomValue(chatModelState.active);
119
+ const context = chatModel?.context ?? draftContext;
120
+ const registry = chatModel?.registry ?? draftRegistry;
106
121
 
107
122
  const editorRef = useRef<ChatEditorController>(null);
108
123
  useEffect(() => {
@@ -157,7 +172,7 @@ export const ChatPrompt = ({
157
172
 
158
173
  // A full queue stops taking prompts: what is typed stays in the editor until the agent takes one up.
159
174
  const queueFull = active && queueSize >= maxQueue;
160
- const canSend = hasText && processor != null && !queueFull;
175
+ const canSend = hasText && context != null && !queueFull;
161
176
 
162
177
  const extensions = useMemo(
163
178
  () => [keymapExtensions, pendingText(), commandsExtension, emptinessExtension],
@@ -165,17 +180,17 @@ export const ChatPrompt = ({
165
180
  );
166
181
 
167
182
  // Submits while a turn is running too: the agent's input queue is feed state, so the prompt is
168
- // queued behind the running turn rather than dropped (`Chat.Root` sends it through the processor's
183
+ // queued behind the running turn rather than dropped (`Chat.Root` sends it through the chat model's
169
184
  // outbox, which queues it while the agent is busy).
170
185
  const handleSubmit = useCallback<NonNullable<ChatEditorProps['onSubmit']>>(
171
186
  (text) => {
172
- if (!processor || queueFull) {
187
+ if (!context || queueFull) {
173
188
  return false;
174
189
  }
175
190
  event.emit({ type: 'submit', text });
176
191
  return true;
177
192
  },
178
- [event, processor, queueFull],
193
+ [event, context, queueFull],
179
194
  );
180
195
 
181
196
  // Routed through `handleSubmit` so the button and the Enter keybinding share one submit path;
@@ -198,19 +213,20 @@ export const ChatPrompt = ({
198
213
  );
199
214
 
200
215
  return (
201
- <div
216
+ <Layout.Flex
217
+ column
202
218
  data-testid='assistant.prompt'
203
219
  role='group'
204
- className={mx(
205
- 'flex flex-col w-full dx-density-md',
220
+ classNames={[
221
+ 'w-full dx-density-md',
206
222
  outline &&
207
223
  'dx-group-surface rounded-sm border border-separator-subtle transition transition-border [&:has(.cm-content:focus)]:border-separator',
208
224
  classNames,
209
- )}
225
+ ]}
210
226
  >
211
- {processor && <ChatMcpErrors processor={processor} />}
227
+ {chatModel && <ChatMcpErrors chatModel={chatModel} />}
212
228
 
213
- <div className='flex p-2 gap-2'>
229
+ <Layout.Flex gap='sm' classNames='p-2'>
214
230
  <ChatStatusIndicator classNames='p-1' preset={preset} error={error} processing={streaming} />
215
231
  <ChatEditor
216
232
  ref={editorRef}
@@ -222,7 +238,7 @@ export const ChatPrompt = ({
222
238
  extensions={extensions}
223
239
  onSubmit={handleSubmit}
224
240
  />
225
- </div>
241
+ </Layout.Flex>
226
242
 
227
243
  {db &&
228
244
  settings && (
@@ -243,20 +259,21 @@ export const ChatPrompt = ({
243
259
  <ChatOptions
244
260
  db={db}
245
261
  chat={chat}
246
- registry={processor?.registry}
247
- context={processor?.context}
262
+ registry={registry}
263
+ context={context}
264
+ started={started}
248
265
  preset={preset}
249
266
  presets={presets}
250
267
  onPresetChange={onPresetChange}
251
268
  />
252
- <div className='flex h-6 grow overflow-x-auto scrollbar-none'>
253
- {processor && <ChatReferences db={db} context={processor.context} />}
254
- </div>
269
+ <Layout.Flex classNames='h-6 grow overflow-x-auto scrollbar-none'>
270
+ {context && <ChatReferences db={db} context={context} />}
271
+ </Layout.Flex>
255
272
  </>
256
273
  }
257
274
  />
258
275
  )}
259
- </div>
276
+ </Layout.Flex>
260
277
  );
261
278
  };
262
279
 
@@ -8,6 +8,7 @@ import { Alarm } from '@dxos/assistant';
8
8
  import { ChatStatus as NaturalChatStatus, formatElapsed } from '@dxos/react-ui-chat';
9
9
  import { Matrix } from '@dxos/react-ui-experimental';
10
10
  import * as Icon from '@dxos/react-ui/Icon';
11
+ import * as Layout from '@dxos/react-ui/Layout';
11
12
  import type * as Util from '@dxos/react-ui/Util';
12
13
  import { type ContentBlock } from '@dxos/types';
13
14
  import { Unit } from '@dxos/util';
@@ -52,8 +53,8 @@ export type ChatStatusViewProps = ChatStreamStatusProps & {
52
53
  * - cumulative session total tokens across all `stats` content blocks
53
54
  */
54
55
  export const ChatStatus = ({ classNames, icon }: ChatStreamStatusProps) => {
55
- // Read `messages` from the chat context (combines `useQuery(queue)` + the processor's
56
- // pending atom) rather than `processor.messages` directly — the latter only contains
56
+ // Read `messages` from the chat context (combines `useQuery(queue)` + the chat model's
57
+ // pending atom) rather than `chatModel.messages` directly — the latter only contains
57
58
  // blocks streamed via the ephemeral `PartialBlock` channel, while finalized blocks
58
59
  // (including the per-turn `stats` block we read for token counts) are submitted to the
59
60
  // feed via `_submitMessage` and only show up through `useQuery`.
@@ -92,7 +93,7 @@ export const ChatStatus = ({ classNames, icon }: ChatStreamStatusProps) => {
92
93
 
93
94
  /**
94
95
  * The pill itself, given resolved values. Split from {@link ChatStatus} so each slot — elapsed,
95
- * tokens, the next alarm — can be mounted and asserted in a story without a live processor.
96
+ * tokens, the next alarm — can be mounted and asserted in a story without a live chat model.
96
97
  */
97
98
  export const ChatStatusView = ({
98
99
  classNames,
@@ -115,7 +116,7 @@ export const ChatStatusView = ({
115
116
  <NaturalChatStatus.Icon>
116
117
  <Matrix
117
118
  classNames='size-5'
118
- dotClassNames='bg-primary-500'
119
+ dotClassNames='bg-primary-bg'
119
120
  dim={4}
120
121
  dotSize={3}
121
122
  count={10}
@@ -125,9 +126,9 @@ export const ChatStatusView = ({
125
126
  </NaturalChatStatus.Icon>
126
127
  )}
127
128
  {show && (
128
- <div className='flex items-center'>
129
+ <Layout.Flex align='center'>
129
130
  {requestTiming && (
130
- <NaturalChatStatus.Text classNames={isRunning && 'text-sky-500'}>
131
+ <NaturalChatStatus.Text classNames={isRunning && 'text-sky-text'}>
131
132
  <Elapsed timing={requestTiming} />
132
133
  </NaturalChatStatus.Text>
133
134
  )}
@@ -163,7 +164,7 @@ export const ChatStatusView = ({
163
164
  </NaturalChatStatus.Text>
164
165
  </>
165
166
  )}
166
- </div>
167
+ </Layout.Flex>
167
168
  )}
168
169
  </NaturalChatStatus.Root>
169
170
  );
@@ -9,13 +9,13 @@ import type * as Skill from '@dxos/compute/Skill';
9
9
  import { type Database, Filter, type Ref } from '@dxos/echo';
10
10
  import { useQuery } from '@dxos/echo-react';
11
11
  import { log } from '@dxos/log';
12
+ import * as Layout from '@dxos/react-ui/Layout';
12
13
  import * as ScrollArea from '@dxos/react-ui/ScrollArea';
13
14
  import * as Util from '@dxos/react-ui/Util';
14
- import { mx } from '@dxos/ui-theme';
15
15
 
16
16
  import { AssistantService } from '#types';
17
17
 
18
- import { type AiChatProcessor } from '../../processor/index.ts';
18
+ import { type ChatModel } from '../../chat-model/index.ts';
19
19
 
20
20
  export type ToolboxProps = {
21
21
  services?: { service: AssistantService.ServiceType }[];
@@ -82,40 +82,39 @@ type SectionProps = {
82
82
 
83
83
  const Section = ({ title, items, striped }: SectionProps) => {
84
84
  const stripeClassNames = 'odd:bg-neutral-50 dark:odd:bg-neutral-800';
85
- const gridClassNames = 'grid grid-cols-[8rem_1fr]';
86
- const subGridClassNames = mx('col-span-full grid grid-cols-subgrid text-xs px-2', striped && stripeClassNames);
85
+ const subGridClassNames = ['text-xs px-2', striped && stripeClassNames];
87
86
 
88
87
  return (
89
88
  <div>
90
89
  <h1 className='px-2 text-sm'>{title}</h1>
91
- <div className={gridClassNames}>
90
+ <Layout.Grid cols={['8rem', 'fill']}>
92
91
  {items.map(({ name, description, subitems }, i) => (
93
92
  <Fragment key={i}>
94
93
  {name && (
95
- <div className={subGridClassNames}>
94
+ <Layout.Grid cols='subgrid' classNames={subGridClassNames}>
96
95
  <div className='truncate text-accent-text'>{name}</div>
97
96
  <div className='line-clamp-2'>{description}</div>
98
- </div>
97
+ </Layout.Grid>
99
98
  )}
100
99
  {subitems?.map(({ name, description }, i) => (
101
- <div key={i} className={mx(subGridClassNames, striped && stripeClassNames)}>
100
+ <Layout.Grid key={i} cols='subgrid' classNames={subGridClassNames}>
102
101
  <div className='truncate'>{name}</div>
103
102
  <div className='line-clamp-3 text-fg-subtle'>{description}</div>
104
- </div>
103
+ </Layout.Grid>
105
104
  ))}
106
105
  </Fragment>
107
106
  ))}
108
- </div>
107
+ </Layout.Grid>
109
108
  </div>
110
109
  );
111
110
  };
112
111
 
113
112
  export type ToolboxPanelProps = Util.ThemedClassName<{
114
113
  db?: Database.Database;
115
- processor?: AiChatProcessor;
114
+ chatModel?: ChatModel;
116
115
  }>;
117
116
 
118
- export const ToolboxPanel = ({ classNames, db, processor }: ToolboxPanelProps) => {
117
+ export const ToolboxPanel = ({ classNames, db, chatModel }: ToolboxPanelProps) => {
119
118
  // Registered services.
120
119
  const services = useQuery(db, Filter.type(AssistantService.ServiceType));
121
120
  const [serviceTools, setServiceTools] = useState<{ service: AssistantService.ServiceType }[]>([]);
@@ -136,7 +135,7 @@ export const ToolboxPanel = ({ classNames, db, processor }: ToolboxPanelProps) =
136
135
  return (
137
136
  <Toolbox
138
137
  classNames={classNames}
139
- skills={processor?.context.getSkills()}
138
+ skills={chatModel?.context.getSkills()}
140
139
  services={serviceTools}
141
140
  functions={functions}
142
141
  />
@@ -15,7 +15,7 @@ import * as UiHooks from '@dxos/react-ui/Hooks';
15
15
  import { meta } from '#meta';
16
16
  import { Assistant, AssistantCapabilities, Ollama } from '#types';
17
17
 
18
- import { presetsForProvider, resolveProvider } from '../../processor/index.ts';
18
+ import { presetsForProvider, resolveProvider } from '../../chat-model/index.ts';
19
19
  import { OllamaModels } from './OllamaModels.tsx';
20
20
 
21
21
  export type AssistantSettingsProps = AppSurface.SettingsProps<Assistant.Settings>;
@@ -336,7 +336,7 @@ export const Send: Story = {
336
336
  /**
337
337
  * Submitting again without waiting for the running turn: the second prompt is QUEUED on the feed and
338
338
  * runs after the first, rather than being dropped (the composer used to ignore a submit while a turn
339
- * was active) or cancelling the turn in flight (`processor.request` interrupts, `enqueue` does not).
339
+ * was active) or cancelling the turn in flight (`chatModel.request` interrupts, `enqueue` does not).
340
340
  *
341
341
  * Deliberately no wait between the two submits — that is the whole case. Both replies landing is what
342
342
  * proves the second prompt survived; the agent-level ordering and mid-turn arrival are pinned
@@ -12,6 +12,7 @@ import * as ToolkitHooks from '@dxos/app-toolkit/Hooks';
12
12
  import type * as Chat from '@dxos/assistant/Chat';
13
13
  import { Obj } from '@dxos/echo';
14
14
  import { useObject } from '@dxos/echo-react';
15
+ import { useIdentity } from '@dxos/halo-react';
15
16
  import * as ClientOperation from '@dxos/plugin-client/ClientOperation';
16
17
  import { useRegistry } from '@dxos/react-client/echo';
17
18
  import { type ChatView } from '@dxos/react-ui-assistant';
@@ -21,7 +22,7 @@ import * as Panel from '@dxos/react-ui/Panel';
21
22
  import { Merge } from '@dxos/util';
22
23
 
23
24
  import { Chat as ChatComponent, type ChatRootProps } from '#components';
24
- import { useChatProcessor, useChatServices, usePlatform, usePresets, useSelectionContext } from '#hooks';
25
+ import { useChatModel, useChatServices, usePlatform, usePresets, useSelectionContext } from '#hooks';
25
26
  import { AssistantCapabilities } from '#types';
26
27
 
27
28
  export type ChatArticleProps = Merge<
@@ -47,7 +48,16 @@ export const ChatArticle = forwardRef<HTMLDivElement, ChatArticleProps>(
47
48
  const runtime = useChatServices({ id: db?.spaceId });
48
49
 
49
50
  const { preset, ...chatProps } = usePresets(settings, chat);
50
- const processor = useChatProcessor({ db, chat, preset, runtime, registry, settings });
51
+ // Every prompt carries the member's DID, so what they say is attributed to them rather than to a name.
52
+ const identity = useIdentity();
53
+ const sender = useMemo(
54
+ () =>
55
+ identity
56
+ ? { identityDid: identity.did, ...(identity.displayName ? { name: identity.displayName } : {}) }
57
+ : undefined,
58
+ [identity?.did, identity?.displayName],
59
+ );
60
+ const chatModel = useChatModel({ db, chat, preset, runtime, registry, settings, sender });
51
61
  const getContext = useSelectionContext(companionTo);
52
62
 
53
63
  // Subscribe to the view type via `useObject` so the thread re-renders when ChatOptions changes it;
@@ -85,7 +95,7 @@ export const ChatArticle = forwardRef<HTMLDivElement, ChatArticleProps>(
85
95
  }, [attendableId]);
86
96
 
87
97
  useEffect(() => {
88
- if (!processor || !attendableId || pendingSubmitted.current) {
98
+ if (!chatModel || !attendableId || pendingSubmitted.current) {
89
99
  return;
90
100
  }
91
101
 
@@ -98,15 +108,15 @@ export const ChatArticle = forwardRef<HTMLDivElement, ChatArticleProps>(
98
108
  return { ...current, pendingPrompts: rest };
99
109
  });
100
110
 
101
- void processor.request({ message: pendingPrompt });
111
+ void chatModel.request({ message: pendingPrompt });
102
112
  }
103
- }, [processor, attendableId, atomRegistry, stateAtom]);
113
+ }, [chatModel, attendableId, atomRegistry, stateAtom]);
104
114
 
105
115
  return (
106
116
  <ChatComponent.Root
107
117
  chat={chat}
108
118
  db={db}
109
- processor={processor}
119
+ chatModel={chatModel}
110
120
  debug={debug}
111
121
  getContext={getContext}
112
122
  onEvent={onEvent}